@attest-it/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-CEE7ONNG.js → chunk-VC3BBBBO.js} +39 -20
- package/dist/chunk-VC3BBBBO.js.map +1 -0
- package/dist/core-alpha.d.ts +1095 -19
- package/dist/core-beta.d.ts +1095 -19
- package/dist/core-public.d.ts +1095 -19
- package/dist/core-unstripped.d.ts +1095 -19
- package/dist/{crypto-VAXWUGKL.js → crypto-CE2YISRD.js} +3 -3
- package/dist/{crypto-VAXWUGKL.js.map → crypto-CE2YISRD.js.map} +1 -1
- package/dist/index.cjs +1320 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1149 -20
- package/dist/index.d.ts +1145 -20
- package/dist/index.js +1263 -54
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/chunk-CEE7ONNG.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -5,12 +5,15 @@ var fs2 = require('fs/promises');
|
|
|
5
5
|
var path2 = require('path');
|
|
6
6
|
var os = require('os');
|
|
7
7
|
var fs = require('fs');
|
|
8
|
+
var ms = require('ms');
|
|
8
9
|
var yaml = require('yaml');
|
|
9
10
|
var zod = require('zod');
|
|
10
|
-
var
|
|
11
|
+
var crypto2 = require('crypto');
|
|
11
12
|
var tinyglobby = require('tinyglobby');
|
|
12
13
|
var canonicalizeNamespace = require('canonicalize');
|
|
13
14
|
|
|
15
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
|
+
|
|
14
17
|
function _interopNamespace(e) {
|
|
15
18
|
if (e && e.__esModule) return e;
|
|
16
19
|
var n = Object.create(null);
|
|
@@ -33,7 +36,8 @@ var fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
|
|
|
33
36
|
var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
|
|
34
37
|
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
35
38
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
36
|
-
var
|
|
39
|
+
var ms__default = /*#__PURE__*/_interopDefault(ms);
|
|
40
|
+
var crypto2__namespace = /*#__PURE__*/_interopNamespace(crypto2);
|
|
37
41
|
var canonicalizeNamespace__namespace = /*#__PURE__*/_interopNamespace(canonicalizeNamespace);
|
|
38
42
|
|
|
39
43
|
var __defProp = Object.defineProperty;
|
|
@@ -188,28 +192,47 @@ async function generateKeyPair(options = {}) {
|
|
|
188
192
|
}
|
|
189
193
|
async function sign(options) {
|
|
190
194
|
await ensureOpenSSLAvailable();
|
|
191
|
-
const { privateKeyPath, data } = options;
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
const { privateKeyPath, keyProvider, keyRef, data } = options;
|
|
196
|
+
let effectiveKeyPath;
|
|
197
|
+
let cleanup;
|
|
198
|
+
if (keyProvider && keyRef) {
|
|
199
|
+
const result = await keyProvider.getPrivateKey(keyRef);
|
|
200
|
+
effectiveKeyPath = result.keyPath;
|
|
201
|
+
cleanup = result.cleanup;
|
|
202
|
+
} else if (privateKeyPath) {
|
|
203
|
+
effectiveKeyPath = privateKeyPath;
|
|
204
|
+
} else {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"Either privateKeyPath or both keyProvider and keyRef must be provided for signing"
|
|
207
|
+
);
|
|
194
208
|
}
|
|
195
|
-
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
196
|
-
const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
|
|
197
|
-
const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
198
|
-
const dataFile = path2__namespace.join(tmpDir, "data.bin");
|
|
199
|
-
const sigFile = path2__namespace.join(tmpDir, "sig.bin");
|
|
200
209
|
try {
|
|
201
|
-
await
|
|
202
|
-
|
|
203
|
-
const result = await runOpenSSL(signArgs);
|
|
204
|
-
if (result.exitCode !== 0) {
|
|
205
|
-
throw new Error(`Failed to sign data: ${result.stderr}`);
|
|
210
|
+
if (!await fileExists(effectiveKeyPath)) {
|
|
211
|
+
throw new Error(`Private key not found: ${effectiveKeyPath}`);
|
|
206
212
|
}
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
214
|
+
const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
|
|
215
|
+
const tmpDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
216
|
+
const dataFile = path2__namespace.join(tmpDir, "data.bin");
|
|
217
|
+
const sigFile = path2__namespace.join(tmpDir, "sig.bin");
|
|
210
218
|
try {
|
|
211
|
-
await fs2__namespace.
|
|
212
|
-
|
|
219
|
+
await fs2__namespace.writeFile(dataFile, processBuffer);
|
|
220
|
+
const signArgs = ["dgst", "-sha256", "-sign", effectiveKeyPath, "-out", sigFile, dataFile];
|
|
221
|
+
const result = await runOpenSSL(signArgs);
|
|
222
|
+
if (result.exitCode !== 0) {
|
|
223
|
+
throw new Error(`Failed to sign data: ${result.stderr}`);
|
|
224
|
+
}
|
|
225
|
+
const sigBuffer = await fs2__namespace.readFile(sigFile);
|
|
226
|
+
return sigBuffer.toString("base64");
|
|
227
|
+
} finally {
|
|
228
|
+
try {
|
|
229
|
+
await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} finally {
|
|
234
|
+
if (cleanup) {
|
|
235
|
+
await cleanup();
|
|
213
236
|
}
|
|
214
237
|
}
|
|
215
238
|
}
|
|
@@ -259,25 +282,82 @@ var init_crypto = __esm({
|
|
|
259
282
|
openSSLChecked = false;
|
|
260
283
|
}
|
|
261
284
|
});
|
|
285
|
+
var keyProviderOptionsSchema = zod.z.object({
|
|
286
|
+
privateKeyPath: zod.z.string().optional(),
|
|
287
|
+
account: zod.z.string().optional(),
|
|
288
|
+
vault: zod.z.string().optional(),
|
|
289
|
+
itemName: zod.z.string().optional()
|
|
290
|
+
}).strict();
|
|
291
|
+
var keyProviderSchema = zod.z.object({
|
|
292
|
+
type: zod.z.enum(["filesystem", "1password"]).or(zod.z.string()),
|
|
293
|
+
options: keyProviderOptionsSchema.optional()
|
|
294
|
+
}).strict();
|
|
295
|
+
var teamMemberSchema = zod.z.object({
|
|
296
|
+
name: zod.z.string().min(1, "Team member name cannot be empty"),
|
|
297
|
+
email: zod.z.string().email().optional(),
|
|
298
|
+
github: zod.z.string().min(1).optional(),
|
|
299
|
+
publicKey: zod.z.string().min(1, "Public key is required")
|
|
300
|
+
}).strict();
|
|
301
|
+
var fingerprintConfigSchema = zod.z.object({
|
|
302
|
+
paths: zod.z.array(zod.z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
|
|
303
|
+
exclude: zod.z.array(zod.z.string().min(1, "Exclude pattern cannot be empty")).optional()
|
|
304
|
+
}).strict();
|
|
305
|
+
var durationSchema = zod.z.string().refine(
|
|
306
|
+
(val) => {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = ms__default.default(val);
|
|
309
|
+
return typeof parsed === "number" && parsed > 0;
|
|
310
|
+
} catch {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
var gateSchema = zod.z.object({
|
|
319
|
+
name: zod.z.string().min(1, "Gate name cannot be empty"),
|
|
320
|
+
description: zod.z.string().min(1, "Gate description cannot be empty"),
|
|
321
|
+
authorizedSigners: zod.z.array(zod.z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
|
|
322
|
+
fingerprint: fingerprintConfigSchema,
|
|
323
|
+
maxAge: durationSchema
|
|
324
|
+
}).strict();
|
|
262
325
|
var settingsSchema = zod.z.object({
|
|
263
326
|
maxAgeDays: zod.z.number().int().positive().default(30),
|
|
264
327
|
publicKeyPath: zod.z.string().default(".attest-it/pubkey.pem"),
|
|
265
328
|
attestationsPath: zod.z.string().default(".attest-it/attestations.json"),
|
|
266
|
-
defaultCommand: zod.z.string().optional()
|
|
329
|
+
defaultCommand: zod.z.string().optional(),
|
|
330
|
+
keyProvider: keyProviderSchema.optional()
|
|
267
331
|
// Note: algorithm field was removed - RSA is the only supported algorithm
|
|
268
332
|
}).passthrough();
|
|
269
333
|
var suiteSchema = zod.z.object({
|
|
334
|
+
// Gate fields (if present, this suite references a gate)
|
|
335
|
+
gate: zod.z.string().optional(),
|
|
336
|
+
// Legacy fingerprint definition (for backward compatibility)
|
|
270
337
|
description: zod.z.string().optional(),
|
|
271
|
-
packages: zod.z.array(zod.z.string().min(1, "Package path cannot be empty")).
|
|
338
|
+
packages: zod.z.array(zod.z.string().min(1, "Package path cannot be empty")).optional(),
|
|
272
339
|
files: zod.z.array(zod.z.string().min(1, "File path cannot be empty")).optional(),
|
|
273
340
|
ignore: zod.z.array(zod.z.string().min(1, "Ignore pattern cannot be empty")).optional(),
|
|
341
|
+
// CLI-specific fields
|
|
274
342
|
command: zod.z.string().optional(),
|
|
343
|
+
timeout: zod.z.string().optional(),
|
|
344
|
+
interactive: zod.z.boolean().optional(),
|
|
345
|
+
// Relationship fields
|
|
275
346
|
invalidates: zod.z.array(zod.z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
|
|
276
347
|
depends_on: zod.z.array(zod.z.string().min(1, "Dependency suite name cannot be empty")).optional()
|
|
277
|
-
}).strict()
|
|
348
|
+
}).strict().refine(
|
|
349
|
+
(suite) => {
|
|
350
|
+
return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
message: "Suite must either reference a gate or define packages for fingerprinting"
|
|
354
|
+
}
|
|
355
|
+
);
|
|
278
356
|
var configSchema = zod.z.object({
|
|
279
357
|
version: zod.z.literal(1),
|
|
280
358
|
settings: settingsSchema.default({}),
|
|
359
|
+
team: zod.z.record(zod.z.string(), teamMemberSchema).optional(),
|
|
360
|
+
gates: zod.z.record(zod.z.string(), gateSchema).optional(),
|
|
281
361
|
suites: zod.z.record(zod.z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
|
|
282
362
|
message: "At least one suite must be defined"
|
|
283
363
|
}),
|
|
@@ -394,32 +474,52 @@ function resolveConfigPaths(config, repoRoot) {
|
|
|
394
474
|
};
|
|
395
475
|
}
|
|
396
476
|
function toAttestItConfig(config) {
|
|
397
|
-
|
|
477
|
+
const result = {
|
|
398
478
|
version: config.version,
|
|
399
479
|
settings: {
|
|
400
480
|
maxAgeDays: config.settings.maxAgeDays,
|
|
401
481
|
publicKeyPath: config.settings.publicKeyPath,
|
|
402
|
-
attestationsPath: config.settings.attestationsPath
|
|
403
|
-
...config.settings.defaultCommand !== void 0 && {
|
|
404
|
-
defaultCommand: config.settings.defaultCommand
|
|
405
|
-
}
|
|
482
|
+
attestationsPath: config.settings.attestationsPath
|
|
406
483
|
},
|
|
407
|
-
suites:
|
|
408
|
-
Object.entries(config.suites).map(([name, suite]) => [
|
|
409
|
-
name,
|
|
410
|
-
{
|
|
411
|
-
packages: suite.packages,
|
|
412
|
-
...suite.description !== void 0 && { description: suite.description },
|
|
413
|
-
...suite.files !== void 0 && { files: suite.files },
|
|
414
|
-
...suite.ignore !== void 0 && { ignore: suite.ignore },
|
|
415
|
-
...suite.command !== void 0 && { command: suite.command },
|
|
416
|
-
...suite.invalidates !== void 0 && { invalidates: suite.invalidates },
|
|
417
|
-
...suite.depends_on !== void 0 && { depends_on: suite.depends_on }
|
|
418
|
-
}
|
|
419
|
-
])
|
|
420
|
-
),
|
|
421
|
-
...config.groups !== void 0 && { groups: config.groups }
|
|
484
|
+
suites: {}
|
|
422
485
|
};
|
|
486
|
+
if (config.settings.defaultCommand !== void 0) {
|
|
487
|
+
result.settings.defaultCommand = config.settings.defaultCommand;
|
|
488
|
+
}
|
|
489
|
+
if (config.settings.keyProvider !== void 0) {
|
|
490
|
+
result.settings.keyProvider = {
|
|
491
|
+
type: config.settings.keyProvider.type,
|
|
492
|
+
...config.settings.keyProvider.options !== void 0 && {
|
|
493
|
+
options: config.settings.keyProvider.options
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
if (config.team !== void 0) {
|
|
498
|
+
result.team = config.team;
|
|
499
|
+
}
|
|
500
|
+
if (config.gates !== void 0) {
|
|
501
|
+
result.gates = config.gates;
|
|
502
|
+
}
|
|
503
|
+
if (config.groups !== void 0) {
|
|
504
|
+
result.groups = config.groups;
|
|
505
|
+
}
|
|
506
|
+
result.suites = Object.fromEntries(
|
|
507
|
+
Object.entries(config.suites).map(([name, suite]) => {
|
|
508
|
+
const mappedSuite = {};
|
|
509
|
+
if (suite.gate !== void 0) mappedSuite.gate = suite.gate;
|
|
510
|
+
if (suite.packages !== void 0) mappedSuite.packages = suite.packages;
|
|
511
|
+
if (suite.description !== void 0) mappedSuite.description = suite.description;
|
|
512
|
+
if (suite.files !== void 0) mappedSuite.files = suite.files;
|
|
513
|
+
if (suite.ignore !== void 0) mappedSuite.ignore = suite.ignore;
|
|
514
|
+
if (suite.command !== void 0) mappedSuite.command = suite.command;
|
|
515
|
+
if (suite.timeout !== void 0) mappedSuite.timeout = suite.timeout;
|
|
516
|
+
if (suite.interactive !== void 0) mappedSuite.interactive = suite.interactive;
|
|
517
|
+
if (suite.invalidates !== void 0) mappedSuite.invalidates = suite.invalidates;
|
|
518
|
+
if (suite.depends_on !== void 0) mappedSuite.depends_on = suite.depends_on;
|
|
519
|
+
return [name, mappedSuite];
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
return result;
|
|
423
523
|
}
|
|
424
524
|
var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
|
|
425
525
|
function sortFiles(files) {
|
|
@@ -440,15 +540,15 @@ function computeFinalFingerprint(fileHashes) {
|
|
|
440
540
|
});
|
|
441
541
|
const hashes = sorted.map((input) => input.hash);
|
|
442
542
|
const concatenated = Buffer.concat(hashes);
|
|
443
|
-
const finalHash =
|
|
543
|
+
const finalHash = crypto2__namespace.createHash("sha256").update(concatenated).digest();
|
|
444
544
|
return `sha256:${finalHash.toString("hex")}`;
|
|
445
545
|
}
|
|
446
546
|
async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
447
547
|
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
448
548
|
return new Promise((resolve3, reject) => {
|
|
449
|
-
const hash2 =
|
|
549
|
+
const hash2 = crypto2__namespace.createHash("sha256");
|
|
450
550
|
hash2.update(normalizedPath);
|
|
451
|
-
hash2.update("
|
|
551
|
+
hash2.update(":");
|
|
452
552
|
const stream = fs__namespace.createReadStream(realPath);
|
|
453
553
|
stream.on("data", (chunk) => {
|
|
454
554
|
hash2.update(chunk);
|
|
@@ -460,17 +560,17 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
460
560
|
});
|
|
461
561
|
}
|
|
462
562
|
const content = await fs__namespace.promises.readFile(realPath);
|
|
463
|
-
const hash =
|
|
563
|
+
const hash = crypto2__namespace.createHash("sha256");
|
|
464
564
|
hash.update(normalizedPath);
|
|
465
|
-
hash.update("
|
|
565
|
+
hash.update(":");
|
|
466
566
|
hash.update(content);
|
|
467
567
|
return hash.digest();
|
|
468
568
|
}
|
|
469
569
|
function hashFileSync(realPath, normalizedPath) {
|
|
470
570
|
const content = fs__namespace.readFileSync(realPath);
|
|
471
|
-
const hash =
|
|
571
|
+
const hash = crypto2__namespace.createHash("sha256");
|
|
472
572
|
hash.update(normalizedPath);
|
|
473
|
-
hash.update("
|
|
573
|
+
hash.update(":");
|
|
474
574
|
hash.update(content);
|
|
475
575
|
return hash.digest();
|
|
476
576
|
}
|
|
@@ -718,22 +818,37 @@ function createAttestation(params) {
|
|
|
718
818
|
return attestation;
|
|
719
819
|
}
|
|
720
820
|
async function writeSignedAttestations(options) {
|
|
721
|
-
const { sign:
|
|
821
|
+
const { sign: sign4 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
822
|
+
const { privateKeyPath, keyProvider, keyRef } = options;
|
|
823
|
+
if (!privateKeyPath && (!keyProvider || !keyRef)) {
|
|
824
|
+
throw new Error(
|
|
825
|
+
"Either privateKeyPath or both keyProvider and keyRef must be provided for signing"
|
|
826
|
+
);
|
|
827
|
+
}
|
|
722
828
|
const canonical = canonicalizeAttestations(options.attestations);
|
|
723
|
-
const
|
|
724
|
-
privateKeyPath: options.privateKeyPath,
|
|
829
|
+
const signOptions = {
|
|
725
830
|
data: canonical
|
|
726
|
-
}
|
|
831
|
+
};
|
|
832
|
+
if (privateKeyPath !== void 0) {
|
|
833
|
+
signOptions.privateKeyPath = privateKeyPath;
|
|
834
|
+
}
|
|
835
|
+
if (keyProvider !== void 0) {
|
|
836
|
+
signOptions.keyProvider = keyProvider;
|
|
837
|
+
}
|
|
838
|
+
if (keyRef !== void 0) {
|
|
839
|
+
signOptions.keyRef = keyRef;
|
|
840
|
+
}
|
|
841
|
+
const signature = await sign4(signOptions);
|
|
727
842
|
await writeAttestations(options.filePath, options.attestations, signature);
|
|
728
843
|
}
|
|
729
844
|
async function readAndVerifyAttestations(options) {
|
|
730
|
-
const { verify:
|
|
845
|
+
const { verify: verify4 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
731
846
|
const file = await readAttestations(options.filePath);
|
|
732
847
|
if (!file) {
|
|
733
848
|
throw new Error(`Attestations file not found: ${options.filePath}`);
|
|
734
849
|
}
|
|
735
850
|
const canonical = canonicalizeAttestations(file.attestations);
|
|
736
|
-
const isValid = await
|
|
851
|
+
const isValid = await verify4({
|
|
737
852
|
publicKeyPath: options.publicKeyPath,
|
|
738
853
|
data: canonical,
|
|
739
854
|
signature: file.signature
|
|
@@ -756,6 +871,123 @@ var SignatureInvalidError = class extends Error {
|
|
|
756
871
|
|
|
757
872
|
// src/index.ts
|
|
758
873
|
init_crypto();
|
|
874
|
+
function isBuffer(value) {
|
|
875
|
+
return Buffer.isBuffer(value);
|
|
876
|
+
}
|
|
877
|
+
function generateKeyPair2() {
|
|
878
|
+
try {
|
|
879
|
+
const keyPair = crypto2__namespace.generateKeyPairSync("ed25519", {
|
|
880
|
+
publicKeyEncoding: {
|
|
881
|
+
type: "spki",
|
|
882
|
+
format: "pem"
|
|
883
|
+
},
|
|
884
|
+
privateKeyEncoding: {
|
|
885
|
+
type: "pkcs8",
|
|
886
|
+
format: "pem"
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
const { publicKey, privateKey } = keyPair;
|
|
890
|
+
if (typeof publicKey !== "string" || typeof privateKey !== "string") {
|
|
891
|
+
throw new Error("Expected keypair to have string keys");
|
|
892
|
+
}
|
|
893
|
+
const publicKeyObj = crypto2__namespace.createPublicKey(publicKey);
|
|
894
|
+
const publicKeyExport = publicKeyObj.export({
|
|
895
|
+
type: "spki",
|
|
896
|
+
format: "der"
|
|
897
|
+
});
|
|
898
|
+
if (!isBuffer(publicKeyExport)) {
|
|
899
|
+
throw new Error("Expected public key export to be a Buffer");
|
|
900
|
+
}
|
|
901
|
+
const rawPublicKey = publicKeyExport.subarray(12);
|
|
902
|
+
const publicKeyBase64 = rawPublicKey.toString("base64");
|
|
903
|
+
return {
|
|
904
|
+
publicKey: publicKeyBase64,
|
|
905
|
+
privateKey
|
|
906
|
+
};
|
|
907
|
+
} catch (err) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Failed to generate Ed25519 keypair: ${err instanceof Error ? err.message : String(err)}`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function sign3(data, privateKeyPem) {
|
|
914
|
+
try {
|
|
915
|
+
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
916
|
+
const privateKeyObj = crypto2__namespace.createPrivateKey(privateKeyPem);
|
|
917
|
+
const signatureResult = crypto2__namespace.sign(null, dataBuffer, privateKeyObj);
|
|
918
|
+
if (!isBuffer(signatureResult)) {
|
|
919
|
+
throw new Error("Expected signature to be a Buffer");
|
|
920
|
+
}
|
|
921
|
+
return signatureResult.toString("base64");
|
|
922
|
+
} catch (err) {
|
|
923
|
+
throw new Error(
|
|
924
|
+
`Failed to sign data with Ed25519: ${err instanceof Error ? err.message : String(err)}`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function verify3(data, signature, publicKeyBase64) {
|
|
929
|
+
try {
|
|
930
|
+
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
931
|
+
const signatureBuffer = Buffer.from(signature, "base64");
|
|
932
|
+
const rawPublicKey = Buffer.from(publicKeyBase64, "base64");
|
|
933
|
+
if (rawPublicKey.length !== 32) {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`Invalid Ed25519 public key length: expected 32 bytes, got ${rawPublicKey.length.toString()}`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
const spkiHeader = Buffer.from([
|
|
939
|
+
48,
|
|
940
|
+
42,
|
|
941
|
+
// SEQUENCE, 42 bytes
|
|
942
|
+
48,
|
|
943
|
+
5,
|
|
944
|
+
// SEQUENCE, 5 bytes
|
|
945
|
+
6,
|
|
946
|
+
3,
|
|
947
|
+
43,
|
|
948
|
+
101,
|
|
949
|
+
112,
|
|
950
|
+
// OID 1.3.101.112 (Ed25519)
|
|
951
|
+
3,
|
|
952
|
+
33,
|
|
953
|
+
0
|
|
954
|
+
// BIT STRING, 33 bytes (32 key + 1 padding)
|
|
955
|
+
]);
|
|
956
|
+
const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
|
|
957
|
+
const publicKeyObj = crypto2__namespace.createPublicKey({
|
|
958
|
+
key: spkiBuffer,
|
|
959
|
+
format: "der",
|
|
960
|
+
type: "spki"
|
|
961
|
+
});
|
|
962
|
+
return crypto2__namespace.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
|
|
963
|
+
} catch (err) {
|
|
964
|
+
if (err instanceof Error && err.message.includes("verification failed")) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
throw new Error(
|
|
968
|
+
`Failed to verify Ed25519 signature: ${err instanceof Error ? err.message : String(err)}`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function getPublicKeyFromPrivate(privateKeyPem) {
|
|
973
|
+
try {
|
|
974
|
+
const privateKeyObj = crypto2__namespace.createPrivateKey(privateKeyPem);
|
|
975
|
+
const publicKeyObj = crypto2__namespace.createPublicKey(privateKeyObj);
|
|
976
|
+
const publicKeyExport = publicKeyObj.export({
|
|
977
|
+
type: "spki",
|
|
978
|
+
format: "der"
|
|
979
|
+
});
|
|
980
|
+
if (!isBuffer(publicKeyExport)) {
|
|
981
|
+
throw new Error("Expected public key export to be a Buffer");
|
|
982
|
+
}
|
|
983
|
+
const rawPublicKey = publicKeyExport.subarray(12);
|
|
984
|
+
return rawPublicKey.toString("base64");
|
|
985
|
+
} catch (err) {
|
|
986
|
+
throw new Error(
|
|
987
|
+
`Failed to extract public key from Ed25519 private key: ${err instanceof Error ? err.message : String(err)}`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
759
991
|
async function verifyAttestations(options) {
|
|
760
992
|
const { config, repoRoot = process.cwd() } = options;
|
|
761
993
|
const errors = [];
|
|
@@ -806,6 +1038,14 @@ async function verifyAttestations(options) {
|
|
|
806
1038
|
}
|
|
807
1039
|
async function verifySuite(options) {
|
|
808
1040
|
const { suiteName, suiteConfig, attestations, maxAgeDays, repoRoot } = options;
|
|
1041
|
+
if (!suiteConfig.packages || suiteConfig.packages.length === 0) {
|
|
1042
|
+
return {
|
|
1043
|
+
suite: suiteName,
|
|
1044
|
+
status: "NEEDS_ATTESTATION",
|
|
1045
|
+
fingerprint: "",
|
|
1046
|
+
message: "Suite configuration missing packages field"
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
809
1049
|
const fingerprintOptions = {
|
|
810
1050
|
packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
|
|
811
1051
|
baseDir: repoRoot,
|
|
@@ -875,39 +1115,1063 @@ function resolvePath(relativePath, baseDir) {
|
|
|
875
1115
|
return path2__namespace.join(baseDir, relativePath);
|
|
876
1116
|
}
|
|
877
1117
|
|
|
1118
|
+
// src/key-provider/filesystem-provider.ts
|
|
1119
|
+
init_crypto();
|
|
1120
|
+
var FilesystemKeyProvider = class {
|
|
1121
|
+
type = "filesystem";
|
|
1122
|
+
displayName = "Filesystem";
|
|
1123
|
+
privateKeyPath;
|
|
1124
|
+
/**
|
|
1125
|
+
* Create a new FilesystemKeyProvider.
|
|
1126
|
+
* @param options - Provider options
|
|
1127
|
+
*/
|
|
1128
|
+
constructor(options = {}) {
|
|
1129
|
+
this.privateKeyPath = options.privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Check if this provider is available.
|
|
1133
|
+
* Filesystem provider is always available.
|
|
1134
|
+
*/
|
|
1135
|
+
async isAvailable() {
|
|
1136
|
+
return Promise.resolve(true);
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Check if a key exists at the given path.
|
|
1140
|
+
* @param keyRef - Path to the private key file
|
|
1141
|
+
*/
|
|
1142
|
+
async keyExists(keyRef) {
|
|
1143
|
+
try {
|
|
1144
|
+
await fs2__namespace.access(keyRef);
|
|
1145
|
+
return true;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Get the private key path for signing.
|
|
1152
|
+
* Returns the path directly with a no-op cleanup function.
|
|
1153
|
+
* @param keyRef - Path to the private key file
|
|
1154
|
+
*/
|
|
1155
|
+
async getPrivateKey(keyRef) {
|
|
1156
|
+
if (!await this.keyExists(keyRef)) {
|
|
1157
|
+
throw new Error(`Private key not found: ${keyRef}`);
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
keyPath: keyRef,
|
|
1161
|
+
// No-op cleanup for filesystem provider
|
|
1162
|
+
cleanup: async () => {
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Generate a new keypair and store on filesystem.
|
|
1168
|
+
* @param options - Key generation options
|
|
1169
|
+
*/
|
|
1170
|
+
async generateKeyPair(options) {
|
|
1171
|
+
const { publicKeyPath, force = false } = options;
|
|
1172
|
+
const result = await generateKeyPair({
|
|
1173
|
+
privatePath: this.privateKeyPath,
|
|
1174
|
+
publicPath: publicKeyPath,
|
|
1175
|
+
force
|
|
1176
|
+
});
|
|
1177
|
+
return {
|
|
1178
|
+
privateKeyRef: result.privatePath,
|
|
1179
|
+
publicKeyPath: result.publicPath,
|
|
1180
|
+
storageDescription: `Filesystem: ${result.privatePath}`
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Get the configuration for this provider.
|
|
1185
|
+
*/
|
|
1186
|
+
getConfig() {
|
|
1187
|
+
return {
|
|
1188
|
+
type: this.type,
|
|
1189
|
+
options: {
|
|
1190
|
+
privateKeyPath: this.privateKeyPath
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
// src/key-provider/one-password-provider.ts
|
|
1197
|
+
init_crypto();
|
|
1198
|
+
var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
1199
|
+
type = "1password";
|
|
1200
|
+
displayName = "1Password";
|
|
1201
|
+
account;
|
|
1202
|
+
vault;
|
|
1203
|
+
itemName;
|
|
1204
|
+
/**
|
|
1205
|
+
* Create a new OnePasswordKeyProvider.
|
|
1206
|
+
* @param options - Provider options
|
|
1207
|
+
*/
|
|
1208
|
+
constructor(options) {
|
|
1209
|
+
if (options.account !== void 0) {
|
|
1210
|
+
this.account = options.account;
|
|
1211
|
+
}
|
|
1212
|
+
this.vault = options.vault;
|
|
1213
|
+
this.itemName = options.itemName;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Check if the 1Password CLI is installed.
|
|
1217
|
+
* @returns True if `op` command is available
|
|
1218
|
+
*/
|
|
1219
|
+
static async isInstalled() {
|
|
1220
|
+
try {
|
|
1221
|
+
await execCommand("op", ["--version"]);
|
|
1222
|
+
return true;
|
|
1223
|
+
} catch {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* List all 1Password accounts.
|
|
1229
|
+
* @returns Array of account information
|
|
1230
|
+
*/
|
|
1231
|
+
static async listAccounts() {
|
|
1232
|
+
try {
|
|
1233
|
+
const output = await execCommand("op", ["account", "list", "--format=json"]);
|
|
1234
|
+
const parsed = JSON.parse(output);
|
|
1235
|
+
if (!Array.isArray(parsed)) {
|
|
1236
|
+
return [];
|
|
1237
|
+
}
|
|
1238
|
+
return parsed;
|
|
1239
|
+
} catch (error) {
|
|
1240
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1241
|
+
console.error("Failed to list 1Password accounts:", error);
|
|
1242
|
+
}
|
|
1243
|
+
return [];
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* List vaults in a specific account.
|
|
1248
|
+
* @param account - Account email (optional if only one account)
|
|
1249
|
+
* @returns Array of vault information
|
|
1250
|
+
*/
|
|
1251
|
+
static async listVaults(account) {
|
|
1252
|
+
try {
|
|
1253
|
+
const args = ["vault", "list", "--format=json"];
|
|
1254
|
+
if (account) {
|
|
1255
|
+
args.push("--account", account);
|
|
1256
|
+
}
|
|
1257
|
+
const output = await execCommand("op", args);
|
|
1258
|
+
const parsed = JSON.parse(output);
|
|
1259
|
+
if (!Array.isArray(parsed)) {
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
return parsed;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (process.env.NODE_ENV !== "production") {
|
|
1265
|
+
console.error("Failed to list 1Password vaults:", error);
|
|
1266
|
+
}
|
|
1267
|
+
return [];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Check if this provider is available.
|
|
1272
|
+
* Requires `op` CLI to be installed and authenticated.
|
|
1273
|
+
*/
|
|
1274
|
+
async isAvailable() {
|
|
1275
|
+
return _OnePasswordKeyProvider.isInstalled();
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Check if a key exists in 1Password.
|
|
1279
|
+
* @param keyRef - Item name in 1Password
|
|
1280
|
+
*/
|
|
1281
|
+
async keyExists(keyRef) {
|
|
1282
|
+
try {
|
|
1283
|
+
const args = ["item", "get", keyRef, "--vault", this.vault, "--format=json"];
|
|
1284
|
+
if (this.account) {
|
|
1285
|
+
args.push("--account", this.account);
|
|
1286
|
+
}
|
|
1287
|
+
await execCommand("op", args);
|
|
1288
|
+
return true;
|
|
1289
|
+
} catch {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Get the private key from 1Password for signing.
|
|
1295
|
+
* Downloads to a temporary file and returns a cleanup function.
|
|
1296
|
+
* @param keyRef - Item name in 1Password
|
|
1297
|
+
* @throws Error if the key does not exist in 1Password
|
|
1298
|
+
*/
|
|
1299
|
+
async getPrivateKey(keyRef) {
|
|
1300
|
+
if (!await this.keyExists(keyRef)) {
|
|
1301
|
+
throw new Error(
|
|
1302
|
+
`Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
1306
|
+
const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1307
|
+
try {
|
|
1308
|
+
const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
|
|
1309
|
+
if (this.account) {
|
|
1310
|
+
args.push("--account", this.account);
|
|
1311
|
+
}
|
|
1312
|
+
await execCommand("op", args);
|
|
1313
|
+
await setKeyPermissions(tempKeyPath);
|
|
1314
|
+
return {
|
|
1315
|
+
keyPath: tempKeyPath,
|
|
1316
|
+
cleanup: async () => {
|
|
1317
|
+
try {
|
|
1318
|
+
await fs2__namespace.unlink(tempKeyPath);
|
|
1319
|
+
await fs2__namespace.rmdir(tempDir);
|
|
1320
|
+
} catch (cleanupError) {
|
|
1321
|
+
console.warn(
|
|
1322
|
+
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
try {
|
|
1329
|
+
await fs2__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1330
|
+
} catch (cleanupError) {
|
|
1331
|
+
console.warn(
|
|
1332
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
throw error;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Generate a new keypair and store private key in 1Password.
|
|
1340
|
+
* Public key is written to filesystem for repository commit.
|
|
1341
|
+
* @param options - Key generation options
|
|
1342
|
+
*/
|
|
1343
|
+
async generateKeyPair(options) {
|
|
1344
|
+
const { publicKeyPath, force = false } = options;
|
|
1345
|
+
const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
|
|
1346
|
+
const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1347
|
+
try {
|
|
1348
|
+
await generateKeyPair({
|
|
1349
|
+
privatePath: tempPrivateKeyPath,
|
|
1350
|
+
publicPath: publicKeyPath,
|
|
1351
|
+
force
|
|
1352
|
+
});
|
|
1353
|
+
const args = [
|
|
1354
|
+
"document",
|
|
1355
|
+
"create",
|
|
1356
|
+
tempPrivateKeyPath,
|
|
1357
|
+
"--title",
|
|
1358
|
+
this.itemName,
|
|
1359
|
+
"--vault",
|
|
1360
|
+
this.vault
|
|
1361
|
+
];
|
|
1362
|
+
if (this.account) {
|
|
1363
|
+
args.push("--account", this.account);
|
|
1364
|
+
}
|
|
1365
|
+
await execCommand("op", args);
|
|
1366
|
+
await fs2__namespace.unlink(tempPrivateKeyPath);
|
|
1367
|
+
await fs2__namespace.rmdir(tempDir);
|
|
1368
|
+
return {
|
|
1369
|
+
privateKeyRef: this.itemName,
|
|
1370
|
+
publicKeyPath,
|
|
1371
|
+
storageDescription: `1Password: ${this.vault}/${this.itemName}`
|
|
1372
|
+
};
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
try {
|
|
1375
|
+
await fs2__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1376
|
+
} catch (cleanupError) {
|
|
1377
|
+
console.warn(
|
|
1378
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
throw error;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Get the configuration for this provider.
|
|
1386
|
+
*/
|
|
1387
|
+
getConfig() {
|
|
1388
|
+
return {
|
|
1389
|
+
type: this.type,
|
|
1390
|
+
options: {
|
|
1391
|
+
...this.account && { account: this.account },
|
|
1392
|
+
vault: this.vault,
|
|
1393
|
+
itemName: this.itemName
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
async function execCommand(command, args) {
|
|
1399
|
+
return new Promise((resolve3, reject) => {
|
|
1400
|
+
const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1401
|
+
let stdout = "";
|
|
1402
|
+
let stderr = "";
|
|
1403
|
+
proc.stdout.on("data", (data) => {
|
|
1404
|
+
stdout += data.toString();
|
|
1405
|
+
});
|
|
1406
|
+
proc.stderr.on("data", (data) => {
|
|
1407
|
+
stderr += data.toString();
|
|
1408
|
+
});
|
|
1409
|
+
proc.on("close", (code) => {
|
|
1410
|
+
if (code === 0) {
|
|
1411
|
+
resolve3(stdout.trim());
|
|
1412
|
+
} else {
|
|
1413
|
+
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
proc.on("error", (error) => {
|
|
1417
|
+
reject(error);
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/key-provider/macos-keychain-provider.ts
|
|
1423
|
+
init_crypto();
|
|
1424
|
+
var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
1425
|
+
type = "macos-keychain";
|
|
1426
|
+
displayName = "macOS Keychain";
|
|
1427
|
+
itemName;
|
|
1428
|
+
static ACCOUNT = "attest-it";
|
|
1429
|
+
/**
|
|
1430
|
+
* Create a new MacOSKeychainKeyProvider.
|
|
1431
|
+
* @param options - Provider options
|
|
1432
|
+
*/
|
|
1433
|
+
constructor(options) {
|
|
1434
|
+
this.itemName = options.itemName;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Check if this provider is available.
|
|
1438
|
+
* Only available on macOS platforms.
|
|
1439
|
+
*/
|
|
1440
|
+
static isAvailable() {
|
|
1441
|
+
return process.platform === "darwin";
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Check if this provider is available on the current system.
|
|
1445
|
+
*/
|
|
1446
|
+
isAvailable() {
|
|
1447
|
+
return Promise.resolve(_MacOSKeychainKeyProvider.isAvailable());
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Check if a key exists in the keychain.
|
|
1451
|
+
* @param keyRef - Item name in keychain
|
|
1452
|
+
*/
|
|
1453
|
+
async keyExists(keyRef) {
|
|
1454
|
+
try {
|
|
1455
|
+
await execCommand2("security", [
|
|
1456
|
+
"find-generic-password",
|
|
1457
|
+
"-a",
|
|
1458
|
+
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1459
|
+
"-s",
|
|
1460
|
+
keyRef
|
|
1461
|
+
]);
|
|
1462
|
+
return true;
|
|
1463
|
+
} catch {
|
|
1464
|
+
return false;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Get the private key from keychain for signing.
|
|
1469
|
+
* Downloads to a temporary file and returns a cleanup function.
|
|
1470
|
+
* @param keyRef - Item name in keychain
|
|
1471
|
+
* @throws Error if the key does not exist in keychain
|
|
1472
|
+
*/
|
|
1473
|
+
async getPrivateKey(keyRef) {
|
|
1474
|
+
if (!await this.keyExists(keyRef)) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
1480
|
+
const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1481
|
+
try {
|
|
1482
|
+
const base64Key = await execCommand2("security", [
|
|
1483
|
+
"find-generic-password",
|
|
1484
|
+
"-a",
|
|
1485
|
+
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1486
|
+
"-s",
|
|
1487
|
+
keyRef,
|
|
1488
|
+
"-w"
|
|
1489
|
+
]);
|
|
1490
|
+
const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
|
|
1491
|
+
await fs2__namespace.writeFile(tempKeyPath, keyContent, { mode: 384 });
|
|
1492
|
+
await setKeyPermissions(tempKeyPath);
|
|
1493
|
+
return {
|
|
1494
|
+
keyPath: tempKeyPath,
|
|
1495
|
+
cleanup: async () => {
|
|
1496
|
+
try {
|
|
1497
|
+
await fs2__namespace.unlink(tempKeyPath);
|
|
1498
|
+
await fs2__namespace.rmdir(tempDir);
|
|
1499
|
+
} catch (cleanupError) {
|
|
1500
|
+
console.warn(
|
|
1501
|
+
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
try {
|
|
1508
|
+
await fs2__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1509
|
+
} catch (cleanupError) {
|
|
1510
|
+
console.warn(
|
|
1511
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
throw error;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Generate a new keypair and store private key in keychain.
|
|
1519
|
+
* Public key is written to filesystem for repository commit.
|
|
1520
|
+
* @param options - Key generation options
|
|
1521
|
+
*/
|
|
1522
|
+
async generateKeyPair(options) {
|
|
1523
|
+
const { publicKeyPath, force = false } = options;
|
|
1524
|
+
const tempDir = await fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
|
|
1525
|
+
const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1526
|
+
try {
|
|
1527
|
+
await generateKeyPair({
|
|
1528
|
+
privatePath: tempPrivateKeyPath,
|
|
1529
|
+
publicPath: publicKeyPath,
|
|
1530
|
+
force
|
|
1531
|
+
});
|
|
1532
|
+
const privateKeyContent = await fs2__namespace.readFile(tempPrivateKeyPath, "utf8");
|
|
1533
|
+
const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
|
|
1534
|
+
await execCommand2("security", [
|
|
1535
|
+
"add-generic-password",
|
|
1536
|
+
"-a",
|
|
1537
|
+
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1538
|
+
"-s",
|
|
1539
|
+
this.itemName,
|
|
1540
|
+
"-w",
|
|
1541
|
+
base64Key,
|
|
1542
|
+
"-T",
|
|
1543
|
+
"",
|
|
1544
|
+
"-U"
|
|
1545
|
+
]);
|
|
1546
|
+
await fs2__namespace.unlink(tempPrivateKeyPath);
|
|
1547
|
+
await fs2__namespace.rmdir(tempDir);
|
|
1548
|
+
return {
|
|
1549
|
+
privateKeyRef: this.itemName,
|
|
1550
|
+
publicKeyPath,
|
|
1551
|
+
storageDescription: `macOS Keychain: ${this.itemName}`
|
|
1552
|
+
};
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
try {
|
|
1555
|
+
await fs2__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1556
|
+
} catch (cleanupError) {
|
|
1557
|
+
console.warn(
|
|
1558
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
throw error;
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Get the configuration for this provider.
|
|
1566
|
+
*/
|
|
1567
|
+
getConfig() {
|
|
1568
|
+
return {
|
|
1569
|
+
type: this.type,
|
|
1570
|
+
options: {
|
|
1571
|
+
itemName: this.itemName
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
async function execCommand2(command, args) {
|
|
1577
|
+
return new Promise((resolve3, reject) => {
|
|
1578
|
+
const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1579
|
+
let stdout = "";
|
|
1580
|
+
let stderr = "";
|
|
1581
|
+
proc.stdout.on("data", (data) => {
|
|
1582
|
+
stdout += data.toString();
|
|
1583
|
+
});
|
|
1584
|
+
proc.stderr.on("data", (data) => {
|
|
1585
|
+
stderr += data.toString();
|
|
1586
|
+
});
|
|
1587
|
+
proc.on("close", (code) => {
|
|
1588
|
+
if (code === 0) {
|
|
1589
|
+
resolve3(stdout.trim());
|
|
1590
|
+
} else {
|
|
1591
|
+
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
proc.on("error", (error) => {
|
|
1595
|
+
reject(error);
|
|
1596
|
+
});
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/key-provider/registry.ts
|
|
1601
|
+
var KeyProviderRegistry = class {
|
|
1602
|
+
static providers = /* @__PURE__ */ new Map();
|
|
1603
|
+
/**
|
|
1604
|
+
* Register a key provider factory.
|
|
1605
|
+
* @param type - Provider type identifier
|
|
1606
|
+
* @param factory - Factory function to create provider instances
|
|
1607
|
+
*/
|
|
1608
|
+
static register(type, factory) {
|
|
1609
|
+
this.providers.set(type, factory);
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* Create a key provider from configuration.
|
|
1613
|
+
* @param config - Provider configuration
|
|
1614
|
+
* @returns A key provider instance
|
|
1615
|
+
* @throws Error if the provider type is not registered
|
|
1616
|
+
*/
|
|
1617
|
+
static create(config) {
|
|
1618
|
+
const factory = this.providers.get(config.type);
|
|
1619
|
+
if (!factory) {
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
return factory(config);
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Get all registered provider types.
|
|
1628
|
+
* @returns Array of provider type identifiers
|
|
1629
|
+
*/
|
|
1630
|
+
static getProviderTypes() {
|
|
1631
|
+
return Array.from(this.providers.keys());
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
KeyProviderRegistry.register("filesystem", (config) => {
|
|
1635
|
+
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
1636
|
+
if (privateKeyPath !== void 0) {
|
|
1637
|
+
return new FilesystemKeyProvider({ privateKeyPath });
|
|
1638
|
+
}
|
|
1639
|
+
return new FilesystemKeyProvider();
|
|
1640
|
+
});
|
|
1641
|
+
KeyProviderRegistry.register("1password", (config) => {
|
|
1642
|
+
const { options } = config;
|
|
1643
|
+
const account = typeof options.account === "string" ? options.account : void 0;
|
|
1644
|
+
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
1645
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1646
|
+
if (!vault || !itemName) {
|
|
1647
|
+
throw new Error("1Password provider requires vault and itemName options");
|
|
1648
|
+
}
|
|
1649
|
+
if (account !== void 0) {
|
|
1650
|
+
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
1651
|
+
}
|
|
1652
|
+
return new OnePasswordKeyProvider({ vault, itemName });
|
|
1653
|
+
});
|
|
1654
|
+
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
1655
|
+
const { options } = config;
|
|
1656
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1657
|
+
if (!itemName) {
|
|
1658
|
+
throw new Error("macOS Keychain provider requires itemName option");
|
|
1659
|
+
}
|
|
1660
|
+
return new MacOSKeychainKeyProvider({ itemName });
|
|
1661
|
+
});
|
|
1662
|
+
var privateKeyRefSchema = zod.z.discriminatedUnion("type", [
|
|
1663
|
+
zod.z.object({
|
|
1664
|
+
type: zod.z.literal("file"),
|
|
1665
|
+
path: zod.z.string().min(1, "File path cannot be empty")
|
|
1666
|
+
}),
|
|
1667
|
+
zod.z.object({
|
|
1668
|
+
type: zod.z.literal("keychain"),
|
|
1669
|
+
service: zod.z.string().min(1, "Service name cannot be empty"),
|
|
1670
|
+
account: zod.z.string().min(1, "Account name cannot be empty")
|
|
1671
|
+
}),
|
|
1672
|
+
zod.z.object({
|
|
1673
|
+
type: zod.z.literal("1password"),
|
|
1674
|
+
account: zod.z.string().optional(),
|
|
1675
|
+
vault: zod.z.string().min(1, "Vault name cannot be empty"),
|
|
1676
|
+
item: zod.z.string().min(1, "Item name cannot be empty"),
|
|
1677
|
+
field: zod.z.string().optional()
|
|
1678
|
+
})
|
|
1679
|
+
]);
|
|
1680
|
+
var identitySchema = zod.z.object({
|
|
1681
|
+
name: zod.z.string().min(1, "Identity name cannot be empty"),
|
|
1682
|
+
email: zod.z.string().optional(),
|
|
1683
|
+
github: zod.z.string().optional(),
|
|
1684
|
+
publicKey: zod.z.string().min(1, "Public key cannot be empty"),
|
|
1685
|
+
privateKey: privateKeyRefSchema
|
|
1686
|
+
}).strict();
|
|
1687
|
+
var localConfigSchema = zod.z.object({
|
|
1688
|
+
activeIdentity: zod.z.string().min(1, "Active identity name cannot be empty"),
|
|
1689
|
+
identities: zod.z.record(zod.z.string(), identitySchema).refine((identities) => Object.keys(identities).length >= 1, {
|
|
1690
|
+
message: "At least one identity must be defined"
|
|
1691
|
+
})
|
|
1692
|
+
}).strict();
|
|
1693
|
+
var LocalConfigValidationError = class extends Error {
|
|
1694
|
+
constructor(message, issues) {
|
|
1695
|
+
super(message);
|
|
1696
|
+
this.issues = issues;
|
|
1697
|
+
this.name = "LocalConfigValidationError";
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
function getLocalConfigPath() {
|
|
1701
|
+
const home = os.homedir();
|
|
1702
|
+
return path2.join(home, ".config", "attest-it", "config.yaml");
|
|
1703
|
+
}
|
|
1704
|
+
function parseLocalConfigContent(content) {
|
|
1705
|
+
let rawConfig;
|
|
1706
|
+
try {
|
|
1707
|
+
rawConfig = yaml.parse(content);
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
throw new LocalConfigValidationError(
|
|
1710
|
+
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`,
|
|
1711
|
+
[]
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
const result = localConfigSchema.safeParse(rawConfig);
|
|
1715
|
+
if (!result.success) {
|
|
1716
|
+
throw new LocalConfigValidationError(
|
|
1717
|
+
"Local configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
1718
|
+
result.error.issues
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
const identities = Object.fromEntries(
|
|
1722
|
+
Object.entries(result.data.identities).map(([key, identity]) => {
|
|
1723
|
+
let privateKey;
|
|
1724
|
+
if (identity.privateKey.type === "1password") {
|
|
1725
|
+
privateKey = {
|
|
1726
|
+
type: "1password",
|
|
1727
|
+
vault: identity.privateKey.vault,
|
|
1728
|
+
item: identity.privateKey.item,
|
|
1729
|
+
...identity.privateKey.account !== void 0 && {
|
|
1730
|
+
account: identity.privateKey.account
|
|
1731
|
+
},
|
|
1732
|
+
...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
|
|
1733
|
+
};
|
|
1734
|
+
} else {
|
|
1735
|
+
privateKey = identity.privateKey;
|
|
1736
|
+
}
|
|
1737
|
+
return [
|
|
1738
|
+
key,
|
|
1739
|
+
{
|
|
1740
|
+
name: identity.name,
|
|
1741
|
+
publicKey: identity.publicKey,
|
|
1742
|
+
privateKey,
|
|
1743
|
+
...identity.email !== void 0 && { email: identity.email },
|
|
1744
|
+
...identity.github !== void 0 && { github: identity.github }
|
|
1745
|
+
}
|
|
1746
|
+
];
|
|
1747
|
+
})
|
|
1748
|
+
);
|
|
1749
|
+
return {
|
|
1750
|
+
activeIdentity: result.data.activeIdentity,
|
|
1751
|
+
identities
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
async function loadLocalConfig(configPath) {
|
|
1755
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1756
|
+
try {
|
|
1757
|
+
const content = await fs2.readFile(resolvedPath, "utf8");
|
|
1758
|
+
return parseLocalConfigContent(content);
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
if (error instanceof LocalConfigValidationError) {
|
|
1761
|
+
throw error;
|
|
1762
|
+
}
|
|
1763
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
throw error;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function loadLocalConfigSync(configPath) {
|
|
1770
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1771
|
+
try {
|
|
1772
|
+
const content = fs.readFileSync(resolvedPath, "utf8");
|
|
1773
|
+
return parseLocalConfigContent(content);
|
|
1774
|
+
} catch (error) {
|
|
1775
|
+
if (error instanceof LocalConfigValidationError) {
|
|
1776
|
+
throw error;
|
|
1777
|
+
}
|
|
1778
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
throw error;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
async function saveLocalConfig(config, configPath) {
|
|
1785
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1786
|
+
const content = yaml.stringify(config);
|
|
1787
|
+
const dir = path2.dirname(resolvedPath);
|
|
1788
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
1789
|
+
await fs2.writeFile(resolvedPath, content, "utf8");
|
|
1790
|
+
}
|
|
1791
|
+
function saveLocalConfigSync(config, configPath) {
|
|
1792
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1793
|
+
const content = yaml.stringify(config);
|
|
1794
|
+
const dir = path2.dirname(resolvedPath);
|
|
1795
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1796
|
+
fs.writeFileSync(resolvedPath, content, "utf8");
|
|
1797
|
+
}
|
|
1798
|
+
function getActiveIdentity(config) {
|
|
1799
|
+
return config.identities[config.activeIdentity];
|
|
1800
|
+
}
|
|
1801
|
+
function isAuthorizedSigner(config, gateId, publicKey) {
|
|
1802
|
+
const gate = config.gates?.[gateId];
|
|
1803
|
+
if (!gate) {
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
const teamMember = findTeamMemberByPublicKey(config, publicKey);
|
|
1807
|
+
if (!teamMember) {
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
const teamMemberSlug = findTeamMemberSlug(config, teamMember);
|
|
1811
|
+
if (!teamMemberSlug) {
|
|
1812
|
+
return false;
|
|
1813
|
+
}
|
|
1814
|
+
return gate.authorizedSigners.includes(teamMemberSlug);
|
|
1815
|
+
}
|
|
1816
|
+
function getAuthorizedSignersForGate(config, gateId) {
|
|
1817
|
+
const gate = config.gates?.[gateId];
|
|
1818
|
+
if (!gate || !config.team) {
|
|
1819
|
+
return [];
|
|
1820
|
+
}
|
|
1821
|
+
const authorizedMembers = [];
|
|
1822
|
+
for (const signerSlug of gate.authorizedSigners) {
|
|
1823
|
+
const member = config.team[signerSlug];
|
|
1824
|
+
if (member) {
|
|
1825
|
+
authorizedMembers.push(member);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return authorizedMembers;
|
|
1829
|
+
}
|
|
1830
|
+
function findTeamMemberByPublicKey(config, publicKey) {
|
|
1831
|
+
if (!config.team) {
|
|
1832
|
+
return void 0;
|
|
1833
|
+
}
|
|
1834
|
+
for (const member of Object.values(config.team)) {
|
|
1835
|
+
if (member.publicKey === publicKey) {
|
|
1836
|
+
return member;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return void 0;
|
|
1840
|
+
}
|
|
1841
|
+
function findTeamMemberSlug(config, teamMember) {
|
|
1842
|
+
if (!config.team) {
|
|
1843
|
+
return void 0;
|
|
1844
|
+
}
|
|
1845
|
+
for (const [slug, member] of Object.entries(config.team)) {
|
|
1846
|
+
if (member === teamMember || member.publicKey === teamMember.publicKey) {
|
|
1847
|
+
return slug;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return void 0;
|
|
1851
|
+
}
|
|
1852
|
+
function getGate(config, gateId) {
|
|
1853
|
+
return config.gates?.[gateId];
|
|
1854
|
+
}
|
|
1855
|
+
var DURATION_PATTERN = /^\d+(\.\d+)?\s*(ms|s|m|h|d|w|y)$/i;
|
|
1856
|
+
function isValidDurationFormat(value) {
|
|
1857
|
+
return DURATION_PATTERN.test(value.trim());
|
|
1858
|
+
}
|
|
1859
|
+
function parseDuration(duration) {
|
|
1860
|
+
if (!isValidDurationFormat(duration)) {
|
|
1861
|
+
throw new Error(`Invalid duration string: ${duration}`);
|
|
1862
|
+
}
|
|
1863
|
+
const result = ms__default.default(duration);
|
|
1864
|
+
if (typeof result !== "number" || result <= 0) {
|
|
1865
|
+
throw new Error(`Invalid duration string: ${duration}`);
|
|
1866
|
+
}
|
|
1867
|
+
return result;
|
|
1868
|
+
}
|
|
1869
|
+
var sealSchema = zod.z.object({
|
|
1870
|
+
gateId: zod.z.string().min(1, "Gate ID cannot be empty"),
|
|
1871
|
+
// Fingerprint format: sha256:<hex> where hex is at least 1 character
|
|
1872
|
+
// Full fingerprints are 64 hex chars, but tests may use shorter values
|
|
1873
|
+
fingerprint: zod.z.string().regex(/^sha256:[a-f0-9]+$/i, "Invalid fingerprint format (expected sha256:<hex>)"),
|
|
1874
|
+
timestamp: zod.z.string().datetime({ message: "Invalid ISO 8601 timestamp" }),
|
|
1875
|
+
sealedBy: zod.z.string().min(1, "Signer slug cannot be empty"),
|
|
1876
|
+
signature: zod.z.string().min(1, "Signature cannot be empty")
|
|
1877
|
+
});
|
|
1878
|
+
var sealsFileSchema = zod.z.object({
|
|
1879
|
+
version: zod.z.literal(1, { errorMap: () => ({ message: "Unsupported seals file version" }) }),
|
|
1880
|
+
seals: zod.z.record(zod.z.string(), sealSchema)
|
|
1881
|
+
});
|
|
1882
|
+
function createSeal(options) {
|
|
1883
|
+
const { gateId, fingerprint, sealedBy, privateKey } = options;
|
|
1884
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1885
|
+
const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
|
|
1886
|
+
const signature = sign3(canonicalString, privateKey);
|
|
1887
|
+
return {
|
|
1888
|
+
gateId,
|
|
1889
|
+
fingerprint,
|
|
1890
|
+
timestamp,
|
|
1891
|
+
sealedBy,
|
|
1892
|
+
signature
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function verifySeal(seal, config) {
|
|
1896
|
+
const { gateId, fingerprint, timestamp, sealedBy, signature } = seal;
|
|
1897
|
+
if (!config.team) {
|
|
1898
|
+
return {
|
|
1899
|
+
valid: false,
|
|
1900
|
+
error: `No team configuration found`
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
const teamMember = config.team[sealedBy];
|
|
1904
|
+
if (!teamMember) {
|
|
1905
|
+
return {
|
|
1906
|
+
valid: false,
|
|
1907
|
+
error: `Team member '${sealedBy}' not found in configuration`
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
|
|
1911
|
+
try {
|
|
1912
|
+
const isValid = verify3(canonicalString, signature, teamMember.publicKey);
|
|
1913
|
+
if (!isValid) {
|
|
1914
|
+
return {
|
|
1915
|
+
valid: false,
|
|
1916
|
+
error: "Signature verification failed"
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
return { valid: true };
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
return {
|
|
1922
|
+
valid: false,
|
|
1923
|
+
error: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
function parseSealsContent(content) {
|
|
1928
|
+
let rawData;
|
|
1929
|
+
try {
|
|
1930
|
+
rawData = JSON.parse(content);
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1933
|
+
}
|
|
1934
|
+
const result = sealsFileSchema.safeParse(rawData);
|
|
1935
|
+
if (!result.success) {
|
|
1936
|
+
const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
1937
|
+
throw new Error(`Invalid seals file:
|
|
1938
|
+
${issues}`);
|
|
1939
|
+
}
|
|
1940
|
+
return result.data;
|
|
1941
|
+
}
|
|
1942
|
+
async function readSeals(dir) {
|
|
1943
|
+
const sealsPath = path2__namespace.join(dir, ".attest-it", "seals.json");
|
|
1944
|
+
try {
|
|
1945
|
+
const content = await fs__namespace.promises.readFile(sealsPath, "utf8");
|
|
1946
|
+
return parseSealsContent(content);
|
|
1947
|
+
} catch (error) {
|
|
1948
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1949
|
+
return {
|
|
1950
|
+
version: 1,
|
|
1951
|
+
seals: {}
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
throw new Error(
|
|
1955
|
+
`Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function readSealsSync(dir) {
|
|
1960
|
+
const sealsPath = path2__namespace.join(dir, ".attest-it", "seals.json");
|
|
1961
|
+
try {
|
|
1962
|
+
const content = fs__namespace.readFileSync(sealsPath, "utf8");
|
|
1963
|
+
return parseSealsContent(content);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1966
|
+
return {
|
|
1967
|
+
version: 1,
|
|
1968
|
+
seals: {}
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
throw new Error(
|
|
1972
|
+
`Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
async function writeSeals(dir, sealsFile) {
|
|
1977
|
+
const attestItDir = path2__namespace.join(dir, ".attest-it");
|
|
1978
|
+
const sealsPath = path2__namespace.join(attestItDir, "seals.json");
|
|
1979
|
+
try {
|
|
1980
|
+
await fs__namespace.promises.mkdir(attestItDir, { recursive: true });
|
|
1981
|
+
const content = JSON.stringify(sealsFile, null, 2) + "\n";
|
|
1982
|
+
await fs__namespace.promises.writeFile(sealsPath, content, "utf8");
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
throw new Error(
|
|
1985
|
+
`Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
function writeSealsSync(dir, sealsFile) {
|
|
1990
|
+
const attestItDir = path2__namespace.join(dir, ".attest-it");
|
|
1991
|
+
const sealsPath = path2__namespace.join(attestItDir, "seals.json");
|
|
1992
|
+
try {
|
|
1993
|
+
fs__namespace.mkdirSync(attestItDir, { recursive: true });
|
|
1994
|
+
const content = JSON.stringify(sealsFile, null, 2) + "\n";
|
|
1995
|
+
fs__namespace.writeFileSync(sealsPath, content, "utf8");
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
throw new Error(
|
|
1998
|
+
`Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/seal/verification.ts
|
|
2004
|
+
function verifyGateSeal(config, gateId, seals, currentFingerprint) {
|
|
2005
|
+
const gate = getGate(config, gateId);
|
|
2006
|
+
if (!gate) {
|
|
2007
|
+
return {
|
|
2008
|
+
gateId,
|
|
2009
|
+
state: "MISSING",
|
|
2010
|
+
message: `Gate '${gateId}' not found in configuration`
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
const seal = seals.seals[gateId];
|
|
2014
|
+
if (!seal) {
|
|
2015
|
+
return {
|
|
2016
|
+
gateId,
|
|
2017
|
+
state: "MISSING",
|
|
2018
|
+
message: `No seal found for gate '${gateId}'`
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
if (seal.fingerprint !== currentFingerprint) {
|
|
2022
|
+
return {
|
|
2023
|
+
gateId,
|
|
2024
|
+
state: "FINGERPRINT_MISMATCH",
|
|
2025
|
+
seal,
|
|
2026
|
+
message: `Fingerprint changed since seal was created`
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
if (!config.team) {
|
|
2030
|
+
return {
|
|
2031
|
+
gateId,
|
|
2032
|
+
state: "UNKNOWN_SIGNER",
|
|
2033
|
+
seal,
|
|
2034
|
+
message: `No team configuration found`
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
const teamMember = config.team[seal.sealedBy];
|
|
2038
|
+
if (!teamMember) {
|
|
2039
|
+
return {
|
|
2040
|
+
gateId,
|
|
2041
|
+
state: "UNKNOWN_SIGNER",
|
|
2042
|
+
seal,
|
|
2043
|
+
message: `Signer '${seal.sealedBy}' not found in team`
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
const authorized = isAuthorizedSigner(config, gateId, teamMember.publicKey);
|
|
2047
|
+
if (!authorized) {
|
|
2048
|
+
return {
|
|
2049
|
+
gateId,
|
|
2050
|
+
state: "UNKNOWN_SIGNER",
|
|
2051
|
+
seal,
|
|
2052
|
+
message: `Signer '${seal.sealedBy}' is not authorized for gate '${gateId}'`
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
const verificationResult = verifySeal(seal, config);
|
|
2056
|
+
if (!verificationResult.valid) {
|
|
2057
|
+
return {
|
|
2058
|
+
gateId,
|
|
2059
|
+
state: "INVALID_SIGNATURE",
|
|
2060
|
+
seal,
|
|
2061
|
+
message: verificationResult.error ?? "Signature verification failed"
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const maxAgeMs = parseDuration(gate.maxAge);
|
|
2066
|
+
const sealTimestamp = new Date(seal.timestamp).getTime();
|
|
2067
|
+
const now = Date.now();
|
|
2068
|
+
const ageMs = now - sealTimestamp;
|
|
2069
|
+
if (ageMs > maxAgeMs) {
|
|
2070
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
2071
|
+
const maxAgeDays = Math.floor(maxAgeMs / (1e3 * 60 * 60 * 24));
|
|
2072
|
+
return {
|
|
2073
|
+
gateId,
|
|
2074
|
+
state: "STALE",
|
|
2075
|
+
seal,
|
|
2076
|
+
message: `Seal is ${ageDays.toString()} days old, exceeds maxAge of ${maxAgeDays.toString()} days`
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
return {
|
|
2081
|
+
gateId,
|
|
2082
|
+
state: "STALE",
|
|
2083
|
+
seal,
|
|
2084
|
+
message: `Cannot verify freshness: invalid maxAge format: ${error instanceof Error ? error.message : String(error)}`
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
gateId,
|
|
2089
|
+
state: "VALID",
|
|
2090
|
+
seal
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function verifyAllSeals(config, seals, fingerprints) {
|
|
2094
|
+
if (!config.gates) {
|
|
2095
|
+
return [];
|
|
2096
|
+
}
|
|
2097
|
+
const results = [];
|
|
2098
|
+
for (const gateId of Object.keys(config.gates)) {
|
|
2099
|
+
const fingerprint = fingerprints[gateId];
|
|
2100
|
+
if (!fingerprint) {
|
|
2101
|
+
results.push({
|
|
2102
|
+
gateId,
|
|
2103
|
+
state: "MISSING",
|
|
2104
|
+
message: `No fingerprint computed for gate '${gateId}'`
|
|
2105
|
+
});
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2108
|
+
const result = verifyGateSeal(config, gateId, seals, fingerprint);
|
|
2109
|
+
results.push(result);
|
|
2110
|
+
}
|
|
2111
|
+
return results;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
878
2114
|
// src/index.ts
|
|
879
2115
|
var version = "0.0.0";
|
|
880
2116
|
|
|
881
2117
|
exports.ConfigNotFoundError = ConfigNotFoundError;
|
|
882
2118
|
exports.ConfigValidationError = ConfigValidationError;
|
|
2119
|
+
exports.FilesystemKeyProvider = FilesystemKeyProvider;
|
|
2120
|
+
exports.KeyProviderRegistry = KeyProviderRegistry;
|
|
2121
|
+
exports.LocalConfigValidationError = LocalConfigValidationError;
|
|
2122
|
+
exports.MacOSKeychainKeyProvider = MacOSKeychainKeyProvider;
|
|
2123
|
+
exports.OnePasswordKeyProvider = OnePasswordKeyProvider;
|
|
883
2124
|
exports.SignatureInvalidError = SignatureInvalidError;
|
|
884
2125
|
exports.canonicalizeAttestations = canonicalizeAttestations;
|
|
885
2126
|
exports.checkOpenSSL = checkOpenSSL;
|
|
886
2127
|
exports.computeFingerprint = computeFingerprint;
|
|
887
2128
|
exports.computeFingerprintSync = computeFingerprintSync;
|
|
888
2129
|
exports.createAttestation = createAttestation;
|
|
2130
|
+
exports.createSeal = createSeal;
|
|
889
2131
|
exports.findAttestation = findAttestation;
|
|
890
2132
|
exports.findConfigPath = findConfigPath;
|
|
2133
|
+
exports.findTeamMemberByPublicKey = findTeamMemberByPublicKey;
|
|
2134
|
+
exports.generateEd25519KeyPair = generateKeyPair2;
|
|
891
2135
|
exports.generateKeyPair = generateKeyPair;
|
|
2136
|
+
exports.getActiveIdentity = getActiveIdentity;
|
|
2137
|
+
exports.getAuthorizedSignersForGate = getAuthorizedSignersForGate;
|
|
892
2138
|
exports.getDefaultPrivateKeyPath = getDefaultPrivateKeyPath;
|
|
893
2139
|
exports.getDefaultPublicKeyPath = getDefaultPublicKeyPath;
|
|
2140
|
+
exports.getGate = getGate;
|
|
2141
|
+
exports.getLocalConfigPath = getLocalConfigPath;
|
|
2142
|
+
exports.getPublicKeyFromPrivate = getPublicKeyFromPrivate;
|
|
2143
|
+
exports.isAuthorizedSigner = isAuthorizedSigner;
|
|
894
2144
|
exports.listPackageFiles = listPackageFiles;
|
|
895
2145
|
exports.loadConfig = loadConfig;
|
|
896
2146
|
exports.loadConfigSync = loadConfigSync;
|
|
2147
|
+
exports.loadLocalConfig = loadLocalConfig;
|
|
2148
|
+
exports.loadLocalConfigSync = loadLocalConfigSync;
|
|
2149
|
+
exports.parseDuration = parseDuration;
|
|
897
2150
|
exports.readAndVerifyAttestations = readAndVerifyAttestations;
|
|
898
2151
|
exports.readAttestations = readAttestations;
|
|
899
2152
|
exports.readAttestationsSync = readAttestationsSync;
|
|
2153
|
+
exports.readSeals = readSeals;
|
|
2154
|
+
exports.readSealsSync = readSealsSync;
|
|
900
2155
|
exports.removeAttestation = removeAttestation;
|
|
901
2156
|
exports.resolveConfigPaths = resolveConfigPaths;
|
|
2157
|
+
exports.saveLocalConfig = saveLocalConfig;
|
|
2158
|
+
exports.saveLocalConfigSync = saveLocalConfigSync;
|
|
902
2159
|
exports.setKeyPermissions = setKeyPermissions;
|
|
903
2160
|
exports.sign = sign;
|
|
2161
|
+
exports.signEd25519 = sign3;
|
|
904
2162
|
exports.toAttestItConfig = toAttestItConfig;
|
|
905
2163
|
exports.upsertAttestation = upsertAttestation;
|
|
906
2164
|
exports.verify = verify;
|
|
2165
|
+
exports.verifyAllSeals = verifyAllSeals;
|
|
907
2166
|
exports.verifyAttestations = verifyAttestations;
|
|
2167
|
+
exports.verifyEd25519 = verify3;
|
|
2168
|
+
exports.verifyGateSeal = verifyGateSeal;
|
|
2169
|
+
exports.verifySeal = verifySeal;
|
|
908
2170
|
exports.version = version;
|
|
909
2171
|
exports.writeAttestations = writeAttestations;
|
|
910
2172
|
exports.writeAttestationsSync = writeAttestationsSync;
|
|
2173
|
+
exports.writeSeals = writeSeals;
|
|
2174
|
+
exports.writeSealsSync = writeSealsSync;
|
|
911
2175
|
exports.writeSignedAttestations = writeSignedAttestations;
|
|
912
2176
|
//# sourceMappingURL=index.cjs.map
|
|
913
2177
|
//# sourceMappingURL=index.cjs.map
|