@attest-it/core 0.0.2 → 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 +1109 -19
- package/dist/core-beta.d.ts +1109 -19
- package/dist/core-public.d.ts +1109 -19
- package/dist/core-unstripped.d.ts +1109 -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 +1324 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1163 -20
- package/dist/index.d.ts +1159 -20
- package/dist/index.js +1267 -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,27 +282,86 @@ 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(),
|
|
275
|
-
|
|
276
|
-
|
|
343
|
+
timeout: zod.z.string().optional(),
|
|
344
|
+
interactive: zod.z.boolean().optional(),
|
|
345
|
+
// Relationship fields
|
|
346
|
+
invalidates: zod.z.array(zod.z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
|
|
347
|
+
depends_on: zod.z.array(zod.z.string().min(1, "Dependency suite name cannot be empty")).optional()
|
|
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
|
+
);
|
|
277
356
|
var configSchema = zod.z.object({
|
|
278
357
|
version: zod.z.literal(1),
|
|
279
358
|
settings: settingsSchema.default({}),
|
|
359
|
+
team: zod.z.record(zod.z.string(), teamMemberSchema).optional(),
|
|
360
|
+
gates: zod.z.record(zod.z.string(), gateSchema).optional(),
|
|
280
361
|
suites: zod.z.record(zod.z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
|
|
281
362
|
message: "At least one suite must be defined"
|
|
282
|
-
})
|
|
363
|
+
}),
|
|
364
|
+
groups: zod.z.record(zod.z.string(), zod.z.array(zod.z.string().min(1, "Suite name in group cannot be empty"))).optional()
|
|
283
365
|
}).strict();
|
|
284
366
|
var ConfigValidationError = class extends Error {
|
|
285
367
|
constructor(message, issues) {
|
|
@@ -392,30 +474,52 @@ function resolveConfigPaths(config, repoRoot) {
|
|
|
392
474
|
};
|
|
393
475
|
}
|
|
394
476
|
function toAttestItConfig(config) {
|
|
395
|
-
|
|
477
|
+
const result = {
|
|
396
478
|
version: config.version,
|
|
397
479
|
settings: {
|
|
398
480
|
maxAgeDays: config.settings.maxAgeDays,
|
|
399
481
|
publicKeyPath: config.settings.publicKeyPath,
|
|
400
|
-
attestationsPath: config.settings.attestationsPath
|
|
401
|
-
...config.settings.defaultCommand !== void 0 && {
|
|
402
|
-
defaultCommand: config.settings.defaultCommand
|
|
403
|
-
}
|
|
482
|
+
attestationsPath: config.settings.attestationsPath
|
|
404
483
|
},
|
|
405
|
-
suites:
|
|
406
|
-
Object.entries(config.suites).map(([name, suite]) => [
|
|
407
|
-
name,
|
|
408
|
-
{
|
|
409
|
-
packages: suite.packages,
|
|
410
|
-
...suite.description !== void 0 && { description: suite.description },
|
|
411
|
-
...suite.files !== void 0 && { files: suite.files },
|
|
412
|
-
...suite.ignore !== void 0 && { ignore: suite.ignore },
|
|
413
|
-
...suite.command !== void 0 && { command: suite.command },
|
|
414
|
-
...suite.invalidates !== void 0 && { invalidates: suite.invalidates }
|
|
415
|
-
}
|
|
416
|
-
])
|
|
417
|
-
)
|
|
484
|
+
suites: {}
|
|
418
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;
|
|
419
523
|
}
|
|
420
524
|
var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
|
|
421
525
|
function sortFiles(files) {
|
|
@@ -436,15 +540,15 @@ function computeFinalFingerprint(fileHashes) {
|
|
|
436
540
|
});
|
|
437
541
|
const hashes = sorted.map((input) => input.hash);
|
|
438
542
|
const concatenated = Buffer.concat(hashes);
|
|
439
|
-
const finalHash =
|
|
543
|
+
const finalHash = crypto2__namespace.createHash("sha256").update(concatenated).digest();
|
|
440
544
|
return `sha256:${finalHash.toString("hex")}`;
|
|
441
545
|
}
|
|
442
546
|
async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
443
547
|
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
444
548
|
return new Promise((resolve3, reject) => {
|
|
445
|
-
const hash2 =
|
|
549
|
+
const hash2 = crypto2__namespace.createHash("sha256");
|
|
446
550
|
hash2.update(normalizedPath);
|
|
447
|
-
hash2.update("
|
|
551
|
+
hash2.update(":");
|
|
448
552
|
const stream = fs__namespace.createReadStream(realPath);
|
|
449
553
|
stream.on("data", (chunk) => {
|
|
450
554
|
hash2.update(chunk);
|
|
@@ -456,17 +560,17 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
456
560
|
});
|
|
457
561
|
}
|
|
458
562
|
const content = await fs__namespace.promises.readFile(realPath);
|
|
459
|
-
const hash =
|
|
563
|
+
const hash = crypto2__namespace.createHash("sha256");
|
|
460
564
|
hash.update(normalizedPath);
|
|
461
|
-
hash.update("
|
|
565
|
+
hash.update(":");
|
|
462
566
|
hash.update(content);
|
|
463
567
|
return hash.digest();
|
|
464
568
|
}
|
|
465
569
|
function hashFileSync(realPath, normalizedPath) {
|
|
466
570
|
const content = fs__namespace.readFileSync(realPath);
|
|
467
|
-
const hash =
|
|
571
|
+
const hash = crypto2__namespace.createHash("sha256");
|
|
468
572
|
hash.update(normalizedPath);
|
|
469
|
-
hash.update("
|
|
573
|
+
hash.update(":");
|
|
470
574
|
hash.update(content);
|
|
471
575
|
return hash.digest();
|
|
472
576
|
}
|
|
@@ -714,22 +818,37 @@ function createAttestation(params) {
|
|
|
714
818
|
return attestation;
|
|
715
819
|
}
|
|
716
820
|
async function writeSignedAttestations(options) {
|
|
717
|
-
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
|
+
}
|
|
718
828
|
const canonical = canonicalizeAttestations(options.attestations);
|
|
719
|
-
const
|
|
720
|
-
privateKeyPath: options.privateKeyPath,
|
|
829
|
+
const signOptions = {
|
|
721
830
|
data: canonical
|
|
722
|
-
}
|
|
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);
|
|
723
842
|
await writeAttestations(options.filePath, options.attestations, signature);
|
|
724
843
|
}
|
|
725
844
|
async function readAndVerifyAttestations(options) {
|
|
726
|
-
const { verify:
|
|
845
|
+
const { verify: verify4 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
727
846
|
const file = await readAttestations(options.filePath);
|
|
728
847
|
if (!file) {
|
|
729
848
|
throw new Error(`Attestations file not found: ${options.filePath}`);
|
|
730
849
|
}
|
|
731
850
|
const canonical = canonicalizeAttestations(file.attestations);
|
|
732
|
-
const isValid = await
|
|
851
|
+
const isValid = await verify4({
|
|
733
852
|
publicKeyPath: options.publicKeyPath,
|
|
734
853
|
data: canonical,
|
|
735
854
|
signature: file.signature
|
|
@@ -752,6 +871,123 @@ var SignatureInvalidError = class extends Error {
|
|
|
752
871
|
|
|
753
872
|
// src/index.ts
|
|
754
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
|
+
}
|
|
755
991
|
async function verifyAttestations(options) {
|
|
756
992
|
const { config, repoRoot = process.cwd() } = options;
|
|
757
993
|
const errors = [];
|
|
@@ -802,6 +1038,14 @@ async function verifyAttestations(options) {
|
|
|
802
1038
|
}
|
|
803
1039
|
async function verifySuite(options) {
|
|
804
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
|
+
}
|
|
805
1049
|
const fingerprintOptions = {
|
|
806
1050
|
packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
|
|
807
1051
|
baseDir: repoRoot,
|
|
@@ -871,39 +1115,1063 @@ function resolvePath(relativePath, baseDir) {
|
|
|
871
1115
|
return path2__namespace.join(baseDir, relativePath);
|
|
872
1116
|
}
|
|
873
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
|
+
|
|
874
2114
|
// src/index.ts
|
|
875
2115
|
var version = "0.0.0";
|
|
876
2116
|
|
|
877
2117
|
exports.ConfigNotFoundError = ConfigNotFoundError;
|
|
878
2118
|
exports.ConfigValidationError = ConfigValidationError;
|
|
2119
|
+
exports.FilesystemKeyProvider = FilesystemKeyProvider;
|
|
2120
|
+
exports.KeyProviderRegistry = KeyProviderRegistry;
|
|
2121
|
+
exports.LocalConfigValidationError = LocalConfigValidationError;
|
|
2122
|
+
exports.MacOSKeychainKeyProvider = MacOSKeychainKeyProvider;
|
|
2123
|
+
exports.OnePasswordKeyProvider = OnePasswordKeyProvider;
|
|
879
2124
|
exports.SignatureInvalidError = SignatureInvalidError;
|
|
880
2125
|
exports.canonicalizeAttestations = canonicalizeAttestations;
|
|
881
2126
|
exports.checkOpenSSL = checkOpenSSL;
|
|
882
2127
|
exports.computeFingerprint = computeFingerprint;
|
|
883
2128
|
exports.computeFingerprintSync = computeFingerprintSync;
|
|
884
2129
|
exports.createAttestation = createAttestation;
|
|
2130
|
+
exports.createSeal = createSeal;
|
|
885
2131
|
exports.findAttestation = findAttestation;
|
|
886
2132
|
exports.findConfigPath = findConfigPath;
|
|
2133
|
+
exports.findTeamMemberByPublicKey = findTeamMemberByPublicKey;
|
|
2134
|
+
exports.generateEd25519KeyPair = generateKeyPair2;
|
|
887
2135
|
exports.generateKeyPair = generateKeyPair;
|
|
2136
|
+
exports.getActiveIdentity = getActiveIdentity;
|
|
2137
|
+
exports.getAuthorizedSignersForGate = getAuthorizedSignersForGate;
|
|
888
2138
|
exports.getDefaultPrivateKeyPath = getDefaultPrivateKeyPath;
|
|
889
2139
|
exports.getDefaultPublicKeyPath = getDefaultPublicKeyPath;
|
|
2140
|
+
exports.getGate = getGate;
|
|
2141
|
+
exports.getLocalConfigPath = getLocalConfigPath;
|
|
2142
|
+
exports.getPublicKeyFromPrivate = getPublicKeyFromPrivate;
|
|
2143
|
+
exports.isAuthorizedSigner = isAuthorizedSigner;
|
|
890
2144
|
exports.listPackageFiles = listPackageFiles;
|
|
891
2145
|
exports.loadConfig = loadConfig;
|
|
892
2146
|
exports.loadConfigSync = loadConfigSync;
|
|
2147
|
+
exports.loadLocalConfig = loadLocalConfig;
|
|
2148
|
+
exports.loadLocalConfigSync = loadLocalConfigSync;
|
|
2149
|
+
exports.parseDuration = parseDuration;
|
|
893
2150
|
exports.readAndVerifyAttestations = readAndVerifyAttestations;
|
|
894
2151
|
exports.readAttestations = readAttestations;
|
|
895
2152
|
exports.readAttestationsSync = readAttestationsSync;
|
|
2153
|
+
exports.readSeals = readSeals;
|
|
2154
|
+
exports.readSealsSync = readSealsSync;
|
|
896
2155
|
exports.removeAttestation = removeAttestation;
|
|
897
2156
|
exports.resolveConfigPaths = resolveConfigPaths;
|
|
2157
|
+
exports.saveLocalConfig = saveLocalConfig;
|
|
2158
|
+
exports.saveLocalConfigSync = saveLocalConfigSync;
|
|
898
2159
|
exports.setKeyPermissions = setKeyPermissions;
|
|
899
2160
|
exports.sign = sign;
|
|
2161
|
+
exports.signEd25519 = sign3;
|
|
900
2162
|
exports.toAttestItConfig = toAttestItConfig;
|
|
901
2163
|
exports.upsertAttestation = upsertAttestation;
|
|
902
2164
|
exports.verify = verify;
|
|
2165
|
+
exports.verifyAllSeals = verifyAllSeals;
|
|
903
2166
|
exports.verifyAttestations = verifyAttestations;
|
|
2167
|
+
exports.verifyEd25519 = verify3;
|
|
2168
|
+
exports.verifyGateSeal = verifyGateSeal;
|
|
2169
|
+
exports.verifySeal = verifySeal;
|
|
904
2170
|
exports.version = version;
|
|
905
2171
|
exports.writeAttestations = writeAttestations;
|
|
906
2172
|
exports.writeAttestationsSync = writeAttestationsSync;
|
|
2173
|
+
exports.writeSeals = writeSeals;
|
|
2174
|
+
exports.writeSealsSync = writeSealsSync;
|
|
907
2175
|
exports.writeSignedAttestations = writeSignedAttestations;
|
|
908
2176
|
//# sourceMappingURL=index.cjs.map
|
|
909
2177
|
//# sourceMappingURL=index.cjs.map
|