@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.js
CHANGED
|
@@ -1,35 +1,97 @@
|
|
|
1
|
-
|
|
1
|
+
import { getDefaultPrivateKeyPath, generateKeyPair, setKeyPermissions } from './chunk-VC3BBBBO.js';
|
|
2
|
+
export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify } from './chunk-VC3BBBBO.js';
|
|
2
3
|
import * as fs from 'fs';
|
|
3
|
-
import { readFileSync } from 'fs';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
4
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
+
import * as fs6 from 'fs/promises';
|
|
6
|
+
import { readFile, mkdir, writeFile } from 'fs/promises';
|
|
7
|
+
import * as path6 from 'path';
|
|
8
|
+
import { join, resolve, dirname } from 'path';
|
|
9
|
+
import ms from 'ms';
|
|
10
|
+
import { stringify, parse } from 'yaml';
|
|
8
11
|
import { z } from 'zod';
|
|
9
|
-
import * as
|
|
12
|
+
import * as crypto2 from 'crypto';
|
|
10
13
|
import { glob, globSync } from 'tinyglobby';
|
|
11
|
-
import * as
|
|
14
|
+
import * as os2 from 'os';
|
|
15
|
+
import { homedir } from 'os';
|
|
12
16
|
import * as canonicalizeNamespace from 'canonicalize';
|
|
17
|
+
import { spawn } from 'child_process';
|
|
13
18
|
|
|
19
|
+
var keyProviderOptionsSchema = z.object({
|
|
20
|
+
privateKeyPath: z.string().optional(),
|
|
21
|
+
account: z.string().optional(),
|
|
22
|
+
vault: z.string().optional(),
|
|
23
|
+
itemName: z.string().optional()
|
|
24
|
+
}).strict();
|
|
25
|
+
var keyProviderSchema = z.object({
|
|
26
|
+
type: z.enum(["filesystem", "1password"]).or(z.string()),
|
|
27
|
+
options: keyProviderOptionsSchema.optional()
|
|
28
|
+
}).strict();
|
|
29
|
+
var teamMemberSchema = z.object({
|
|
30
|
+
name: z.string().min(1, "Team member name cannot be empty"),
|
|
31
|
+
email: z.string().email().optional(),
|
|
32
|
+
github: z.string().min(1).optional(),
|
|
33
|
+
publicKey: z.string().min(1, "Public key is required")
|
|
34
|
+
}).strict();
|
|
35
|
+
var fingerprintConfigSchema = z.object({
|
|
36
|
+
paths: z.array(z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
|
|
37
|
+
exclude: z.array(z.string().min(1, "Exclude pattern cannot be empty")).optional()
|
|
38
|
+
}).strict();
|
|
39
|
+
var durationSchema = z.string().refine(
|
|
40
|
+
(val) => {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = ms(val);
|
|
43
|
+
return typeof parsed === "number" && parsed > 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
var gateSchema = z.object({
|
|
53
|
+
name: z.string().min(1, "Gate name cannot be empty"),
|
|
54
|
+
description: z.string().min(1, "Gate description cannot be empty"),
|
|
55
|
+
authorizedSigners: z.array(z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
|
|
56
|
+
fingerprint: fingerprintConfigSchema,
|
|
57
|
+
maxAge: durationSchema
|
|
58
|
+
}).strict();
|
|
14
59
|
var settingsSchema = z.object({
|
|
15
60
|
maxAgeDays: z.number().int().positive().default(30),
|
|
16
61
|
publicKeyPath: z.string().default(".attest-it/pubkey.pem"),
|
|
17
62
|
attestationsPath: z.string().default(".attest-it/attestations.json"),
|
|
18
|
-
defaultCommand: z.string().optional()
|
|
63
|
+
defaultCommand: z.string().optional(),
|
|
64
|
+
keyProvider: keyProviderSchema.optional()
|
|
19
65
|
// Note: algorithm field was removed - RSA is the only supported algorithm
|
|
20
66
|
}).passthrough();
|
|
21
67
|
var suiteSchema = z.object({
|
|
68
|
+
// Gate fields (if present, this suite references a gate)
|
|
69
|
+
gate: z.string().optional(),
|
|
70
|
+
// Legacy fingerprint definition (for backward compatibility)
|
|
22
71
|
description: z.string().optional(),
|
|
23
|
-
packages: z.array(z.string().min(1, "Package path cannot be empty")).
|
|
72
|
+
packages: z.array(z.string().min(1, "Package path cannot be empty")).optional(),
|
|
24
73
|
files: z.array(z.string().min(1, "File path cannot be empty")).optional(),
|
|
25
74
|
ignore: z.array(z.string().min(1, "Ignore pattern cannot be empty")).optional(),
|
|
75
|
+
// CLI-specific fields
|
|
26
76
|
command: z.string().optional(),
|
|
77
|
+
timeout: z.string().optional(),
|
|
78
|
+
interactive: z.boolean().optional(),
|
|
79
|
+
// Relationship fields
|
|
27
80
|
invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
|
|
28
81
|
depends_on: z.array(z.string().min(1, "Dependency suite name cannot be empty")).optional()
|
|
29
|
-
}).strict()
|
|
82
|
+
}).strict().refine(
|
|
83
|
+
(suite) => {
|
|
84
|
+
return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
message: "Suite must either reference a gate or define packages for fingerprinting"
|
|
88
|
+
}
|
|
89
|
+
);
|
|
30
90
|
var configSchema = z.object({
|
|
31
91
|
version: z.literal(1),
|
|
32
92
|
settings: settingsSchema.default({}),
|
|
93
|
+
team: z.record(z.string(), teamMemberSchema).optional(),
|
|
94
|
+
gates: z.record(z.string(), gateSchema).optional(),
|
|
33
95
|
suites: z.record(z.string(), suiteSchema).refine((suites) => Object.keys(suites).length >= 1, {
|
|
34
96
|
message: "At least one suite must be defined"
|
|
35
97
|
}),
|
|
@@ -146,32 +208,52 @@ function resolveConfigPaths(config, repoRoot) {
|
|
|
146
208
|
};
|
|
147
209
|
}
|
|
148
210
|
function toAttestItConfig(config) {
|
|
149
|
-
|
|
211
|
+
const result = {
|
|
150
212
|
version: config.version,
|
|
151
213
|
settings: {
|
|
152
214
|
maxAgeDays: config.settings.maxAgeDays,
|
|
153
215
|
publicKeyPath: config.settings.publicKeyPath,
|
|
154
|
-
attestationsPath: config.settings.attestationsPath
|
|
155
|
-
...config.settings.defaultCommand !== void 0 && {
|
|
156
|
-
defaultCommand: config.settings.defaultCommand
|
|
157
|
-
}
|
|
216
|
+
attestationsPath: config.settings.attestationsPath
|
|
158
217
|
},
|
|
159
|
-
suites:
|
|
160
|
-
Object.entries(config.suites).map(([name, suite]) => [
|
|
161
|
-
name,
|
|
162
|
-
{
|
|
163
|
-
packages: suite.packages,
|
|
164
|
-
...suite.description !== void 0 && { description: suite.description },
|
|
165
|
-
...suite.files !== void 0 && { files: suite.files },
|
|
166
|
-
...suite.ignore !== void 0 && { ignore: suite.ignore },
|
|
167
|
-
...suite.command !== void 0 && { command: suite.command },
|
|
168
|
-
...suite.invalidates !== void 0 && { invalidates: suite.invalidates },
|
|
169
|
-
...suite.depends_on !== void 0 && { depends_on: suite.depends_on }
|
|
170
|
-
}
|
|
171
|
-
])
|
|
172
|
-
),
|
|
173
|
-
...config.groups !== void 0 && { groups: config.groups }
|
|
218
|
+
suites: {}
|
|
174
219
|
};
|
|
220
|
+
if (config.settings.defaultCommand !== void 0) {
|
|
221
|
+
result.settings.defaultCommand = config.settings.defaultCommand;
|
|
222
|
+
}
|
|
223
|
+
if (config.settings.keyProvider !== void 0) {
|
|
224
|
+
result.settings.keyProvider = {
|
|
225
|
+
type: config.settings.keyProvider.type,
|
|
226
|
+
...config.settings.keyProvider.options !== void 0 && {
|
|
227
|
+
options: config.settings.keyProvider.options
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (config.team !== void 0) {
|
|
232
|
+
result.team = config.team;
|
|
233
|
+
}
|
|
234
|
+
if (config.gates !== void 0) {
|
|
235
|
+
result.gates = config.gates;
|
|
236
|
+
}
|
|
237
|
+
if (config.groups !== void 0) {
|
|
238
|
+
result.groups = config.groups;
|
|
239
|
+
}
|
|
240
|
+
result.suites = Object.fromEntries(
|
|
241
|
+
Object.entries(config.suites).map(([name, suite]) => {
|
|
242
|
+
const mappedSuite = {};
|
|
243
|
+
if (suite.gate !== void 0) mappedSuite.gate = suite.gate;
|
|
244
|
+
if (suite.packages !== void 0) mappedSuite.packages = suite.packages;
|
|
245
|
+
if (suite.description !== void 0) mappedSuite.description = suite.description;
|
|
246
|
+
if (suite.files !== void 0) mappedSuite.files = suite.files;
|
|
247
|
+
if (suite.ignore !== void 0) mappedSuite.ignore = suite.ignore;
|
|
248
|
+
if (suite.command !== void 0) mappedSuite.command = suite.command;
|
|
249
|
+
if (suite.timeout !== void 0) mappedSuite.timeout = suite.timeout;
|
|
250
|
+
if (suite.interactive !== void 0) mappedSuite.interactive = suite.interactive;
|
|
251
|
+
if (suite.invalidates !== void 0) mappedSuite.invalidates = suite.invalidates;
|
|
252
|
+
if (suite.depends_on !== void 0) mappedSuite.depends_on = suite.depends_on;
|
|
253
|
+
return [name, mappedSuite];
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
return result;
|
|
175
257
|
}
|
|
176
258
|
var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
|
|
177
259
|
function sortFiles(files) {
|
|
@@ -182,7 +264,7 @@ function sortFiles(files) {
|
|
|
182
264
|
});
|
|
183
265
|
}
|
|
184
266
|
function normalizePath(filePath) {
|
|
185
|
-
return filePath.split(
|
|
267
|
+
return filePath.split(path6.sep).join("/");
|
|
186
268
|
}
|
|
187
269
|
function computeFinalFingerprint(fileHashes) {
|
|
188
270
|
const sorted = [...fileHashes].sort((a, b) => {
|
|
@@ -192,15 +274,15 @@ function computeFinalFingerprint(fileHashes) {
|
|
|
192
274
|
});
|
|
193
275
|
const hashes = sorted.map((input) => input.hash);
|
|
194
276
|
const concatenated = Buffer.concat(hashes);
|
|
195
|
-
const finalHash =
|
|
277
|
+
const finalHash = crypto2.createHash("sha256").update(concatenated).digest();
|
|
196
278
|
return `sha256:${finalHash.toString("hex")}`;
|
|
197
279
|
}
|
|
198
280
|
async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
199
281
|
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
200
282
|
return new Promise((resolve3, reject) => {
|
|
201
|
-
const hash2 =
|
|
283
|
+
const hash2 = crypto2.createHash("sha256");
|
|
202
284
|
hash2.update(normalizedPath);
|
|
203
|
-
hash2.update("
|
|
285
|
+
hash2.update(":");
|
|
204
286
|
const stream = fs.createReadStream(realPath);
|
|
205
287
|
stream.on("data", (chunk) => {
|
|
206
288
|
hash2.update(chunk);
|
|
@@ -212,17 +294,17 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
212
294
|
});
|
|
213
295
|
}
|
|
214
296
|
const content = await fs.promises.readFile(realPath);
|
|
215
|
-
const hash =
|
|
297
|
+
const hash = crypto2.createHash("sha256");
|
|
216
298
|
hash.update(normalizedPath);
|
|
217
|
-
hash.update("
|
|
299
|
+
hash.update(":");
|
|
218
300
|
hash.update(content);
|
|
219
301
|
return hash.digest();
|
|
220
302
|
}
|
|
221
303
|
function hashFileSync(realPath, normalizedPath) {
|
|
222
304
|
const content = fs.readFileSync(realPath);
|
|
223
|
-
const hash =
|
|
305
|
+
const hash = crypto2.createHash("sha256");
|
|
224
306
|
hash.update(normalizedPath);
|
|
225
|
-
hash.update("
|
|
307
|
+
hash.update(":");
|
|
226
308
|
hash.update(content);
|
|
227
309
|
return hash.digest();
|
|
228
310
|
}
|
|
@@ -232,7 +314,7 @@ function validateOptions(options) {
|
|
|
232
314
|
}
|
|
233
315
|
const baseDir = options.baseDir ?? process.cwd();
|
|
234
316
|
for (const pkg of options.packages) {
|
|
235
|
-
const pkgPath =
|
|
317
|
+
const pkgPath = path6.resolve(baseDir, pkg);
|
|
236
318
|
if (!fs.existsSync(pkgPath)) {
|
|
237
319
|
throw new Error(`Package path does not exist: ${pkgPath}`);
|
|
238
320
|
}
|
|
@@ -246,7 +328,7 @@ async function computeFingerprint(options) {
|
|
|
246
328
|
const fileHashCache = /* @__PURE__ */ new Map();
|
|
247
329
|
const fileHashInputs = [];
|
|
248
330
|
for (const file of sortedFiles) {
|
|
249
|
-
const filePath =
|
|
331
|
+
const filePath = path6.resolve(baseDir, file);
|
|
250
332
|
let realPath = filePath;
|
|
251
333
|
let stats = await fs.promises.lstat(filePath);
|
|
252
334
|
if (stats.isSymbolicLink()) {
|
|
@@ -289,7 +371,7 @@ function computeFingerprintSync(options) {
|
|
|
289
371
|
const fileHashCache = /* @__PURE__ */ new Map();
|
|
290
372
|
const fileHashInputs = [];
|
|
291
373
|
for (const file of sortedFiles) {
|
|
292
|
-
const filePath =
|
|
374
|
+
const filePath = path6.resolve(baseDir, file);
|
|
293
375
|
let realPath = filePath;
|
|
294
376
|
let stats = fs.lstatSync(filePath);
|
|
295
377
|
if (stats.isSymbolicLink()) {
|
|
@@ -416,7 +498,7 @@ async function writeAttestations(filePath, attestations, signature) {
|
|
|
416
498
|
signature
|
|
417
499
|
};
|
|
418
500
|
attestationsFileSchema.parse(fileContent);
|
|
419
|
-
const dir =
|
|
501
|
+
const dir = path6.dirname(filePath);
|
|
420
502
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
421
503
|
const json = JSON.stringify(fileContent, null, 2);
|
|
422
504
|
await fs.promises.writeFile(filePath, json, "utf-8");
|
|
@@ -428,7 +510,7 @@ function writeAttestationsSync(filePath, attestations, signature) {
|
|
|
428
510
|
signature
|
|
429
511
|
};
|
|
430
512
|
attestationsFileSchema.parse(fileContent);
|
|
431
|
-
const dir =
|
|
513
|
+
const dir = path6.dirname(filePath);
|
|
432
514
|
fs.mkdirSync(dir, { recursive: true });
|
|
433
515
|
const json = JSON.stringify(fileContent, null, 2);
|
|
434
516
|
fs.writeFileSync(filePath, json, "utf-8");
|
|
@@ -462,7 +544,7 @@ function createAttestation(params) {
|
|
|
462
544
|
suite: params.suite,
|
|
463
545
|
fingerprint: params.fingerprint,
|
|
464
546
|
attestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
465
|
-
attestedBy: params.attestedBy ??
|
|
547
|
+
attestedBy: params.attestedBy ?? os2.userInfo().username,
|
|
466
548
|
command: params.command,
|
|
467
549
|
exitCode: 0
|
|
468
550
|
};
|
|
@@ -470,22 +552,37 @@ function createAttestation(params) {
|
|
|
470
552
|
return attestation;
|
|
471
553
|
}
|
|
472
554
|
async function writeSignedAttestations(options) {
|
|
473
|
-
const { sign:
|
|
555
|
+
const { sign: sign4 } = await import('./crypto-CE2YISRD.js');
|
|
556
|
+
const { privateKeyPath, keyProvider, keyRef } = options;
|
|
557
|
+
if (!privateKeyPath && (!keyProvider || !keyRef)) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
"Either privateKeyPath or both keyProvider and keyRef must be provided for signing"
|
|
560
|
+
);
|
|
561
|
+
}
|
|
474
562
|
const canonical = canonicalizeAttestations(options.attestations);
|
|
475
|
-
const
|
|
476
|
-
privateKeyPath: options.privateKeyPath,
|
|
563
|
+
const signOptions = {
|
|
477
564
|
data: canonical
|
|
478
|
-
}
|
|
565
|
+
};
|
|
566
|
+
if (privateKeyPath !== void 0) {
|
|
567
|
+
signOptions.privateKeyPath = privateKeyPath;
|
|
568
|
+
}
|
|
569
|
+
if (keyProvider !== void 0) {
|
|
570
|
+
signOptions.keyProvider = keyProvider;
|
|
571
|
+
}
|
|
572
|
+
if (keyRef !== void 0) {
|
|
573
|
+
signOptions.keyRef = keyRef;
|
|
574
|
+
}
|
|
575
|
+
const signature = await sign4(signOptions);
|
|
479
576
|
await writeAttestations(options.filePath, options.attestations, signature);
|
|
480
577
|
}
|
|
481
578
|
async function readAndVerifyAttestations(options) {
|
|
482
|
-
const { verify:
|
|
579
|
+
const { verify: verify4 } = await import('./crypto-CE2YISRD.js');
|
|
483
580
|
const file = await readAttestations(options.filePath);
|
|
484
581
|
if (!file) {
|
|
485
582
|
throw new Error(`Attestations file not found: ${options.filePath}`);
|
|
486
583
|
}
|
|
487
584
|
const canonical = canonicalizeAttestations(file.attestations);
|
|
488
|
-
const isValid = await
|
|
585
|
+
const isValid = await verify4({
|
|
489
586
|
publicKeyPath: options.publicKeyPath,
|
|
490
587
|
data: canonical,
|
|
491
588
|
signature: file.signature
|
|
@@ -505,6 +602,123 @@ var SignatureInvalidError = class extends Error {
|
|
|
505
602
|
this.name = "SignatureInvalidError";
|
|
506
603
|
}
|
|
507
604
|
};
|
|
605
|
+
function isBuffer(value) {
|
|
606
|
+
return Buffer.isBuffer(value);
|
|
607
|
+
}
|
|
608
|
+
function generateKeyPair2() {
|
|
609
|
+
try {
|
|
610
|
+
const keyPair = crypto2.generateKeyPairSync("ed25519", {
|
|
611
|
+
publicKeyEncoding: {
|
|
612
|
+
type: "spki",
|
|
613
|
+
format: "pem"
|
|
614
|
+
},
|
|
615
|
+
privateKeyEncoding: {
|
|
616
|
+
type: "pkcs8",
|
|
617
|
+
format: "pem"
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
const { publicKey, privateKey } = keyPair;
|
|
621
|
+
if (typeof publicKey !== "string" || typeof privateKey !== "string") {
|
|
622
|
+
throw new Error("Expected keypair to have string keys");
|
|
623
|
+
}
|
|
624
|
+
const publicKeyObj = crypto2.createPublicKey(publicKey);
|
|
625
|
+
const publicKeyExport = publicKeyObj.export({
|
|
626
|
+
type: "spki",
|
|
627
|
+
format: "der"
|
|
628
|
+
});
|
|
629
|
+
if (!isBuffer(publicKeyExport)) {
|
|
630
|
+
throw new Error("Expected public key export to be a Buffer");
|
|
631
|
+
}
|
|
632
|
+
const rawPublicKey = publicKeyExport.subarray(12);
|
|
633
|
+
const publicKeyBase64 = rawPublicKey.toString("base64");
|
|
634
|
+
return {
|
|
635
|
+
publicKey: publicKeyBase64,
|
|
636
|
+
privateKey
|
|
637
|
+
};
|
|
638
|
+
} catch (err) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`Failed to generate Ed25519 keypair: ${err instanceof Error ? err.message : String(err)}`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function sign3(data, privateKeyPem) {
|
|
645
|
+
try {
|
|
646
|
+
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
647
|
+
const privateKeyObj = crypto2.createPrivateKey(privateKeyPem);
|
|
648
|
+
const signatureResult = crypto2.sign(null, dataBuffer, privateKeyObj);
|
|
649
|
+
if (!isBuffer(signatureResult)) {
|
|
650
|
+
throw new Error("Expected signature to be a Buffer");
|
|
651
|
+
}
|
|
652
|
+
return signatureResult.toString("base64");
|
|
653
|
+
} catch (err) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Failed to sign data with Ed25519: ${err instanceof Error ? err.message : String(err)}`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function verify3(data, signature, publicKeyBase64) {
|
|
660
|
+
try {
|
|
661
|
+
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
662
|
+
const signatureBuffer = Buffer.from(signature, "base64");
|
|
663
|
+
const rawPublicKey = Buffer.from(publicKeyBase64, "base64");
|
|
664
|
+
if (rawPublicKey.length !== 32) {
|
|
665
|
+
throw new Error(
|
|
666
|
+
`Invalid Ed25519 public key length: expected 32 bytes, got ${rawPublicKey.length.toString()}`
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const spkiHeader = Buffer.from([
|
|
670
|
+
48,
|
|
671
|
+
42,
|
|
672
|
+
// SEQUENCE, 42 bytes
|
|
673
|
+
48,
|
|
674
|
+
5,
|
|
675
|
+
// SEQUENCE, 5 bytes
|
|
676
|
+
6,
|
|
677
|
+
3,
|
|
678
|
+
43,
|
|
679
|
+
101,
|
|
680
|
+
112,
|
|
681
|
+
// OID 1.3.101.112 (Ed25519)
|
|
682
|
+
3,
|
|
683
|
+
33,
|
|
684
|
+
0
|
|
685
|
+
// BIT STRING, 33 bytes (32 key + 1 padding)
|
|
686
|
+
]);
|
|
687
|
+
const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
|
|
688
|
+
const publicKeyObj = crypto2.createPublicKey({
|
|
689
|
+
key: spkiBuffer,
|
|
690
|
+
format: "der",
|
|
691
|
+
type: "spki"
|
|
692
|
+
});
|
|
693
|
+
return crypto2.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
if (err instanceof Error && err.message.includes("verification failed")) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
throw new Error(
|
|
699
|
+
`Failed to verify Ed25519 signature: ${err instanceof Error ? err.message : String(err)}`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function getPublicKeyFromPrivate(privateKeyPem) {
|
|
704
|
+
try {
|
|
705
|
+
const privateKeyObj = crypto2.createPrivateKey(privateKeyPem);
|
|
706
|
+
const publicKeyObj = crypto2.createPublicKey(privateKeyObj);
|
|
707
|
+
const publicKeyExport = publicKeyObj.export({
|
|
708
|
+
type: "spki",
|
|
709
|
+
format: "der"
|
|
710
|
+
});
|
|
711
|
+
if (!isBuffer(publicKeyExport)) {
|
|
712
|
+
throw new Error("Expected public key export to be a Buffer");
|
|
713
|
+
}
|
|
714
|
+
const rawPublicKey = publicKeyExport.subarray(12);
|
|
715
|
+
return rawPublicKey.toString("base64");
|
|
716
|
+
} catch (err) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
`Failed to extract public key from Ed25519 private key: ${err instanceof Error ? err.message : String(err)}`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
508
722
|
async function verifyAttestations(options) {
|
|
509
723
|
const { config, repoRoot = process.cwd() } = options;
|
|
510
724
|
const errors = [];
|
|
@@ -555,6 +769,14 @@ async function verifyAttestations(options) {
|
|
|
555
769
|
}
|
|
556
770
|
async function verifySuite(options) {
|
|
557
771
|
const { suiteName, suiteConfig, attestations, maxAgeDays, repoRoot } = options;
|
|
772
|
+
if (!suiteConfig.packages || suiteConfig.packages.length === 0) {
|
|
773
|
+
return {
|
|
774
|
+
suite: suiteName,
|
|
775
|
+
status: "NEEDS_ATTESTATION",
|
|
776
|
+
fingerprint: "",
|
|
777
|
+
message: "Suite configuration missing packages field"
|
|
778
|
+
};
|
|
779
|
+
}
|
|
558
780
|
const fingerprintOptions = {
|
|
559
781
|
packages: suiteConfig.packages.map((p) => resolvePath(p, repoRoot)),
|
|
560
782
|
baseDir: repoRoot,
|
|
@@ -618,15 +840,1002 @@ function checkInvalidationChains(config, results) {
|
|
|
618
840
|
}
|
|
619
841
|
}
|
|
620
842
|
function resolvePath(relativePath, baseDir) {
|
|
621
|
-
if (
|
|
843
|
+
if (path6.isAbsolute(relativePath)) {
|
|
622
844
|
return relativePath;
|
|
623
845
|
}
|
|
624
|
-
return
|
|
846
|
+
return path6.join(baseDir, relativePath);
|
|
847
|
+
}
|
|
848
|
+
var FilesystemKeyProvider = class {
|
|
849
|
+
type = "filesystem";
|
|
850
|
+
displayName = "Filesystem";
|
|
851
|
+
privateKeyPath;
|
|
852
|
+
/**
|
|
853
|
+
* Create a new FilesystemKeyProvider.
|
|
854
|
+
* @param options - Provider options
|
|
855
|
+
*/
|
|
856
|
+
constructor(options = {}) {
|
|
857
|
+
this.privateKeyPath = options.privateKeyPath ?? getDefaultPrivateKeyPath();
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Check if this provider is available.
|
|
861
|
+
* Filesystem provider is always available.
|
|
862
|
+
*/
|
|
863
|
+
async isAvailable() {
|
|
864
|
+
return Promise.resolve(true);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Check if a key exists at the given path.
|
|
868
|
+
* @param keyRef - Path to the private key file
|
|
869
|
+
*/
|
|
870
|
+
async keyExists(keyRef) {
|
|
871
|
+
try {
|
|
872
|
+
await fs6.access(keyRef);
|
|
873
|
+
return true;
|
|
874
|
+
} catch {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Get the private key path for signing.
|
|
880
|
+
* Returns the path directly with a no-op cleanup function.
|
|
881
|
+
* @param keyRef - Path to the private key file
|
|
882
|
+
*/
|
|
883
|
+
async getPrivateKey(keyRef) {
|
|
884
|
+
if (!await this.keyExists(keyRef)) {
|
|
885
|
+
throw new Error(`Private key not found: ${keyRef}`);
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
keyPath: keyRef,
|
|
889
|
+
// No-op cleanup for filesystem provider
|
|
890
|
+
cleanup: async () => {
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Generate a new keypair and store on filesystem.
|
|
896
|
+
* @param options - Key generation options
|
|
897
|
+
*/
|
|
898
|
+
async generateKeyPair(options) {
|
|
899
|
+
const { publicKeyPath, force = false } = options;
|
|
900
|
+
const result = await generateKeyPair({
|
|
901
|
+
privatePath: this.privateKeyPath,
|
|
902
|
+
publicPath: publicKeyPath,
|
|
903
|
+
force
|
|
904
|
+
});
|
|
905
|
+
return {
|
|
906
|
+
privateKeyRef: result.privatePath,
|
|
907
|
+
publicKeyPath: result.publicPath,
|
|
908
|
+
storageDescription: `Filesystem: ${result.privatePath}`
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Get the configuration for this provider.
|
|
913
|
+
*/
|
|
914
|
+
getConfig() {
|
|
915
|
+
return {
|
|
916
|
+
type: this.type,
|
|
917
|
+
options: {
|
|
918
|
+
privateKeyPath: this.privateKeyPath
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
924
|
+
type = "1password";
|
|
925
|
+
displayName = "1Password";
|
|
926
|
+
account;
|
|
927
|
+
vault;
|
|
928
|
+
itemName;
|
|
929
|
+
/**
|
|
930
|
+
* Create a new OnePasswordKeyProvider.
|
|
931
|
+
* @param options - Provider options
|
|
932
|
+
*/
|
|
933
|
+
constructor(options) {
|
|
934
|
+
if (options.account !== void 0) {
|
|
935
|
+
this.account = options.account;
|
|
936
|
+
}
|
|
937
|
+
this.vault = options.vault;
|
|
938
|
+
this.itemName = options.itemName;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Check if the 1Password CLI is installed.
|
|
942
|
+
* @returns True if `op` command is available
|
|
943
|
+
*/
|
|
944
|
+
static async isInstalled() {
|
|
945
|
+
try {
|
|
946
|
+
await execCommand("op", ["--version"]);
|
|
947
|
+
return true;
|
|
948
|
+
} catch {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* List all 1Password accounts.
|
|
954
|
+
* @returns Array of account information
|
|
955
|
+
*/
|
|
956
|
+
static async listAccounts() {
|
|
957
|
+
try {
|
|
958
|
+
const output = await execCommand("op", ["account", "list", "--format=json"]);
|
|
959
|
+
const parsed = JSON.parse(output);
|
|
960
|
+
if (!Array.isArray(parsed)) {
|
|
961
|
+
return [];
|
|
962
|
+
}
|
|
963
|
+
return parsed;
|
|
964
|
+
} catch (error) {
|
|
965
|
+
if (process.env.NODE_ENV !== "production") {
|
|
966
|
+
console.error("Failed to list 1Password accounts:", error);
|
|
967
|
+
}
|
|
968
|
+
return [];
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* List vaults in a specific account.
|
|
973
|
+
* @param account - Account email (optional if only one account)
|
|
974
|
+
* @returns Array of vault information
|
|
975
|
+
*/
|
|
976
|
+
static async listVaults(account) {
|
|
977
|
+
try {
|
|
978
|
+
const args = ["vault", "list", "--format=json"];
|
|
979
|
+
if (account) {
|
|
980
|
+
args.push("--account", account);
|
|
981
|
+
}
|
|
982
|
+
const output = await execCommand("op", args);
|
|
983
|
+
const parsed = JSON.parse(output);
|
|
984
|
+
if (!Array.isArray(parsed)) {
|
|
985
|
+
return [];
|
|
986
|
+
}
|
|
987
|
+
return parsed;
|
|
988
|
+
} catch (error) {
|
|
989
|
+
if (process.env.NODE_ENV !== "production") {
|
|
990
|
+
console.error("Failed to list 1Password vaults:", error);
|
|
991
|
+
}
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Check if this provider is available.
|
|
997
|
+
* Requires `op` CLI to be installed and authenticated.
|
|
998
|
+
*/
|
|
999
|
+
async isAvailable() {
|
|
1000
|
+
return _OnePasswordKeyProvider.isInstalled();
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Check if a key exists in 1Password.
|
|
1004
|
+
* @param keyRef - Item name in 1Password
|
|
1005
|
+
*/
|
|
1006
|
+
async keyExists(keyRef) {
|
|
1007
|
+
try {
|
|
1008
|
+
const args = ["item", "get", keyRef, "--vault", this.vault, "--format=json"];
|
|
1009
|
+
if (this.account) {
|
|
1010
|
+
args.push("--account", this.account);
|
|
1011
|
+
}
|
|
1012
|
+
await execCommand("op", args);
|
|
1013
|
+
return true;
|
|
1014
|
+
} catch {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Get the private key from 1Password for signing.
|
|
1020
|
+
* Downloads to a temporary file and returns a cleanup function.
|
|
1021
|
+
* @param keyRef - Item name in 1Password
|
|
1022
|
+
* @throws Error if the key does not exist in 1Password
|
|
1023
|
+
*/
|
|
1024
|
+
async getPrivateKey(keyRef) {
|
|
1025
|
+
if (!await this.keyExists(keyRef)) {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
`Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
1031
|
+
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
1032
|
+
try {
|
|
1033
|
+
const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
|
|
1034
|
+
if (this.account) {
|
|
1035
|
+
args.push("--account", this.account);
|
|
1036
|
+
}
|
|
1037
|
+
await execCommand("op", args);
|
|
1038
|
+
await setKeyPermissions(tempKeyPath);
|
|
1039
|
+
return {
|
|
1040
|
+
keyPath: tempKeyPath,
|
|
1041
|
+
cleanup: async () => {
|
|
1042
|
+
try {
|
|
1043
|
+
await fs6.unlink(tempKeyPath);
|
|
1044
|
+
await fs6.rmdir(tempDir);
|
|
1045
|
+
} catch (cleanupError) {
|
|
1046
|
+
console.warn(
|
|
1047
|
+
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
try {
|
|
1054
|
+
await fs6.rm(tempDir, { recursive: true, force: true });
|
|
1055
|
+
} catch (cleanupError) {
|
|
1056
|
+
console.warn(
|
|
1057
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Generate a new keypair and store private key in 1Password.
|
|
1065
|
+
* Public key is written to filesystem for repository commit.
|
|
1066
|
+
* @param options - Key generation options
|
|
1067
|
+
*/
|
|
1068
|
+
async generateKeyPair(options) {
|
|
1069
|
+
const { publicKeyPath, force = false } = options;
|
|
1070
|
+
const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
1071
|
+
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
1072
|
+
try {
|
|
1073
|
+
await generateKeyPair({
|
|
1074
|
+
privatePath: tempPrivateKeyPath,
|
|
1075
|
+
publicPath: publicKeyPath,
|
|
1076
|
+
force
|
|
1077
|
+
});
|
|
1078
|
+
const args = [
|
|
1079
|
+
"document",
|
|
1080
|
+
"create",
|
|
1081
|
+
tempPrivateKeyPath,
|
|
1082
|
+
"--title",
|
|
1083
|
+
this.itemName,
|
|
1084
|
+
"--vault",
|
|
1085
|
+
this.vault
|
|
1086
|
+
];
|
|
1087
|
+
if (this.account) {
|
|
1088
|
+
args.push("--account", this.account);
|
|
1089
|
+
}
|
|
1090
|
+
await execCommand("op", args);
|
|
1091
|
+
await fs6.unlink(tempPrivateKeyPath);
|
|
1092
|
+
await fs6.rmdir(tempDir);
|
|
1093
|
+
return {
|
|
1094
|
+
privateKeyRef: this.itemName,
|
|
1095
|
+
publicKeyPath,
|
|
1096
|
+
storageDescription: `1Password: ${this.vault}/${this.itemName}`
|
|
1097
|
+
};
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
try {
|
|
1100
|
+
await fs6.rm(tempDir, { recursive: true, force: true });
|
|
1101
|
+
} catch (cleanupError) {
|
|
1102
|
+
console.warn(
|
|
1103
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
throw error;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Get the configuration for this provider.
|
|
1111
|
+
*/
|
|
1112
|
+
getConfig() {
|
|
1113
|
+
return {
|
|
1114
|
+
type: this.type,
|
|
1115
|
+
options: {
|
|
1116
|
+
...this.account && { account: this.account },
|
|
1117
|
+
vault: this.vault,
|
|
1118
|
+
itemName: this.itemName
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
async function execCommand(command, args) {
|
|
1124
|
+
return new Promise((resolve3, reject) => {
|
|
1125
|
+
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1126
|
+
let stdout = "";
|
|
1127
|
+
let stderr = "";
|
|
1128
|
+
proc.stdout.on("data", (data) => {
|
|
1129
|
+
stdout += data.toString();
|
|
1130
|
+
});
|
|
1131
|
+
proc.stderr.on("data", (data) => {
|
|
1132
|
+
stderr += data.toString();
|
|
1133
|
+
});
|
|
1134
|
+
proc.on("close", (code) => {
|
|
1135
|
+
if (code === 0) {
|
|
1136
|
+
resolve3(stdout.trim());
|
|
1137
|
+
} else {
|
|
1138
|
+
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
proc.on("error", (error) => {
|
|
1142
|
+
reject(error);
|
|
1143
|
+
});
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
1147
|
+
type = "macos-keychain";
|
|
1148
|
+
displayName = "macOS Keychain";
|
|
1149
|
+
itemName;
|
|
1150
|
+
static ACCOUNT = "attest-it";
|
|
1151
|
+
/**
|
|
1152
|
+
* Create a new MacOSKeychainKeyProvider.
|
|
1153
|
+
* @param options - Provider options
|
|
1154
|
+
*/
|
|
1155
|
+
constructor(options) {
|
|
1156
|
+
this.itemName = options.itemName;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Check if this provider is available.
|
|
1160
|
+
* Only available on macOS platforms.
|
|
1161
|
+
*/
|
|
1162
|
+
static isAvailable() {
|
|
1163
|
+
return process.platform === "darwin";
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Check if this provider is available on the current system.
|
|
1167
|
+
*/
|
|
1168
|
+
isAvailable() {
|
|
1169
|
+
return Promise.resolve(_MacOSKeychainKeyProvider.isAvailable());
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Check if a key exists in the keychain.
|
|
1173
|
+
* @param keyRef - Item name in keychain
|
|
1174
|
+
*/
|
|
1175
|
+
async keyExists(keyRef) {
|
|
1176
|
+
try {
|
|
1177
|
+
await execCommand2("security", [
|
|
1178
|
+
"find-generic-password",
|
|
1179
|
+
"-a",
|
|
1180
|
+
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1181
|
+
"-s",
|
|
1182
|
+
keyRef
|
|
1183
|
+
]);
|
|
1184
|
+
return true;
|
|
1185
|
+
} catch {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Get the private key from keychain for signing.
|
|
1191
|
+
* Downloads to a temporary file and returns a cleanup function.
|
|
1192
|
+
* @param keyRef - Item name in keychain
|
|
1193
|
+
* @throws Error if the key does not exist in keychain
|
|
1194
|
+
*/
|
|
1195
|
+
async getPrivateKey(keyRef) {
|
|
1196
|
+
if (!await this.keyExists(keyRef)) {
|
|
1197
|
+
throw new Error(
|
|
1198
|
+
`Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
1202
|
+
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
1203
|
+
try {
|
|
1204
|
+
const base64Key = await execCommand2("security", [
|
|
1205
|
+
"find-generic-password",
|
|
1206
|
+
"-a",
|
|
1207
|
+
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1208
|
+
"-s",
|
|
1209
|
+
keyRef,
|
|
1210
|
+
"-w"
|
|
1211
|
+
]);
|
|
1212
|
+
const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
|
|
1213
|
+
await fs6.writeFile(tempKeyPath, keyContent, { mode: 384 });
|
|
1214
|
+
await setKeyPermissions(tempKeyPath);
|
|
1215
|
+
return {
|
|
1216
|
+
keyPath: tempKeyPath,
|
|
1217
|
+
cleanup: async () => {
|
|
1218
|
+
try {
|
|
1219
|
+
await fs6.unlink(tempKeyPath);
|
|
1220
|
+
await fs6.rmdir(tempDir);
|
|
1221
|
+
} catch (cleanupError) {
|
|
1222
|
+
console.warn(
|
|
1223
|
+
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
try {
|
|
1230
|
+
await fs6.rm(tempDir, { recursive: true, force: true });
|
|
1231
|
+
} catch (cleanupError) {
|
|
1232
|
+
console.warn(
|
|
1233
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Generate a new keypair and store private key in keychain.
|
|
1241
|
+
* Public key is written to filesystem for repository commit.
|
|
1242
|
+
* @param options - Key generation options
|
|
1243
|
+
*/
|
|
1244
|
+
async generateKeyPair(options) {
|
|
1245
|
+
const { publicKeyPath, force = false } = options;
|
|
1246
|
+
const tempDir = await fs6.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
1247
|
+
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
1248
|
+
try {
|
|
1249
|
+
await generateKeyPair({
|
|
1250
|
+
privatePath: tempPrivateKeyPath,
|
|
1251
|
+
publicPath: publicKeyPath,
|
|
1252
|
+
force
|
|
1253
|
+
});
|
|
1254
|
+
const privateKeyContent = await fs6.readFile(tempPrivateKeyPath, "utf8");
|
|
1255
|
+
const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
|
|
1256
|
+
await execCommand2("security", [
|
|
1257
|
+
"add-generic-password",
|
|
1258
|
+
"-a",
|
|
1259
|
+
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1260
|
+
"-s",
|
|
1261
|
+
this.itemName,
|
|
1262
|
+
"-w",
|
|
1263
|
+
base64Key,
|
|
1264
|
+
"-T",
|
|
1265
|
+
"",
|
|
1266
|
+
"-U"
|
|
1267
|
+
]);
|
|
1268
|
+
await fs6.unlink(tempPrivateKeyPath);
|
|
1269
|
+
await fs6.rmdir(tempDir);
|
|
1270
|
+
return {
|
|
1271
|
+
privateKeyRef: this.itemName,
|
|
1272
|
+
publicKeyPath,
|
|
1273
|
+
storageDescription: `macOS Keychain: ${this.itemName}`
|
|
1274
|
+
};
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
try {
|
|
1277
|
+
await fs6.rm(tempDir, { recursive: true, force: true });
|
|
1278
|
+
} catch (cleanupError) {
|
|
1279
|
+
console.warn(
|
|
1280
|
+
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Get the configuration for this provider.
|
|
1288
|
+
*/
|
|
1289
|
+
getConfig() {
|
|
1290
|
+
return {
|
|
1291
|
+
type: this.type,
|
|
1292
|
+
options: {
|
|
1293
|
+
itemName: this.itemName
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
async function execCommand2(command, args) {
|
|
1299
|
+
return new Promise((resolve3, reject) => {
|
|
1300
|
+
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1301
|
+
let stdout = "";
|
|
1302
|
+
let stderr = "";
|
|
1303
|
+
proc.stdout.on("data", (data) => {
|
|
1304
|
+
stdout += data.toString();
|
|
1305
|
+
});
|
|
1306
|
+
proc.stderr.on("data", (data) => {
|
|
1307
|
+
stderr += data.toString();
|
|
1308
|
+
});
|
|
1309
|
+
proc.on("close", (code) => {
|
|
1310
|
+
if (code === 0) {
|
|
1311
|
+
resolve3(stdout.trim());
|
|
1312
|
+
} else {
|
|
1313
|
+
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
proc.on("error", (error) => {
|
|
1317
|
+
reject(error);
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/key-provider/registry.ts
|
|
1323
|
+
var KeyProviderRegistry = class {
|
|
1324
|
+
static providers = /* @__PURE__ */ new Map();
|
|
1325
|
+
/**
|
|
1326
|
+
* Register a key provider factory.
|
|
1327
|
+
* @param type - Provider type identifier
|
|
1328
|
+
* @param factory - Factory function to create provider instances
|
|
1329
|
+
*/
|
|
1330
|
+
static register(type, factory) {
|
|
1331
|
+
this.providers.set(type, factory);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Create a key provider from configuration.
|
|
1335
|
+
* @param config - Provider configuration
|
|
1336
|
+
* @returns A key provider instance
|
|
1337
|
+
* @throws Error if the provider type is not registered
|
|
1338
|
+
*/
|
|
1339
|
+
static create(config) {
|
|
1340
|
+
const factory = this.providers.get(config.type);
|
|
1341
|
+
if (!factory) {
|
|
1342
|
+
throw new Error(
|
|
1343
|
+
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
return factory(config);
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* Get all registered provider types.
|
|
1350
|
+
* @returns Array of provider type identifiers
|
|
1351
|
+
*/
|
|
1352
|
+
static getProviderTypes() {
|
|
1353
|
+
return Array.from(this.providers.keys());
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
KeyProviderRegistry.register("filesystem", (config) => {
|
|
1357
|
+
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
1358
|
+
if (privateKeyPath !== void 0) {
|
|
1359
|
+
return new FilesystemKeyProvider({ privateKeyPath });
|
|
1360
|
+
}
|
|
1361
|
+
return new FilesystemKeyProvider();
|
|
1362
|
+
});
|
|
1363
|
+
KeyProviderRegistry.register("1password", (config) => {
|
|
1364
|
+
const { options } = config;
|
|
1365
|
+
const account = typeof options.account === "string" ? options.account : void 0;
|
|
1366
|
+
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
1367
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1368
|
+
if (!vault || !itemName) {
|
|
1369
|
+
throw new Error("1Password provider requires vault and itemName options");
|
|
1370
|
+
}
|
|
1371
|
+
if (account !== void 0) {
|
|
1372
|
+
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
1373
|
+
}
|
|
1374
|
+
return new OnePasswordKeyProvider({ vault, itemName });
|
|
1375
|
+
});
|
|
1376
|
+
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
1377
|
+
const { options } = config;
|
|
1378
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1379
|
+
if (!itemName) {
|
|
1380
|
+
throw new Error("macOS Keychain provider requires itemName option");
|
|
1381
|
+
}
|
|
1382
|
+
return new MacOSKeychainKeyProvider({ itemName });
|
|
1383
|
+
});
|
|
1384
|
+
var privateKeyRefSchema = z.discriminatedUnion("type", [
|
|
1385
|
+
z.object({
|
|
1386
|
+
type: z.literal("file"),
|
|
1387
|
+
path: z.string().min(1, "File path cannot be empty")
|
|
1388
|
+
}),
|
|
1389
|
+
z.object({
|
|
1390
|
+
type: z.literal("keychain"),
|
|
1391
|
+
service: z.string().min(1, "Service name cannot be empty"),
|
|
1392
|
+
account: z.string().min(1, "Account name cannot be empty")
|
|
1393
|
+
}),
|
|
1394
|
+
z.object({
|
|
1395
|
+
type: z.literal("1password"),
|
|
1396
|
+
account: z.string().optional(),
|
|
1397
|
+
vault: z.string().min(1, "Vault name cannot be empty"),
|
|
1398
|
+
item: z.string().min(1, "Item name cannot be empty"),
|
|
1399
|
+
field: z.string().optional()
|
|
1400
|
+
})
|
|
1401
|
+
]);
|
|
1402
|
+
var identitySchema = z.object({
|
|
1403
|
+
name: z.string().min(1, "Identity name cannot be empty"),
|
|
1404
|
+
email: z.string().optional(),
|
|
1405
|
+
github: z.string().optional(),
|
|
1406
|
+
publicKey: z.string().min(1, "Public key cannot be empty"),
|
|
1407
|
+
privateKey: privateKeyRefSchema
|
|
1408
|
+
}).strict();
|
|
1409
|
+
var localConfigSchema = z.object({
|
|
1410
|
+
activeIdentity: z.string().min(1, "Active identity name cannot be empty"),
|
|
1411
|
+
identities: z.record(z.string(), identitySchema).refine((identities) => Object.keys(identities).length >= 1, {
|
|
1412
|
+
message: "At least one identity must be defined"
|
|
1413
|
+
})
|
|
1414
|
+
}).strict();
|
|
1415
|
+
var LocalConfigValidationError = class extends Error {
|
|
1416
|
+
constructor(message, issues) {
|
|
1417
|
+
super(message);
|
|
1418
|
+
this.issues = issues;
|
|
1419
|
+
this.name = "LocalConfigValidationError";
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
function getLocalConfigPath() {
|
|
1423
|
+
const home = homedir();
|
|
1424
|
+
return join(home, ".config", "attest-it", "config.yaml");
|
|
1425
|
+
}
|
|
1426
|
+
function parseLocalConfigContent(content) {
|
|
1427
|
+
let rawConfig;
|
|
1428
|
+
try {
|
|
1429
|
+
rawConfig = parse(content);
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
throw new LocalConfigValidationError(
|
|
1432
|
+
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`,
|
|
1433
|
+
[]
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
const result = localConfigSchema.safeParse(rawConfig);
|
|
1437
|
+
if (!result.success) {
|
|
1438
|
+
throw new LocalConfigValidationError(
|
|
1439
|
+
"Local configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
1440
|
+
result.error.issues
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
const identities = Object.fromEntries(
|
|
1444
|
+
Object.entries(result.data.identities).map(([key, identity]) => {
|
|
1445
|
+
let privateKey;
|
|
1446
|
+
if (identity.privateKey.type === "1password") {
|
|
1447
|
+
privateKey = {
|
|
1448
|
+
type: "1password",
|
|
1449
|
+
vault: identity.privateKey.vault,
|
|
1450
|
+
item: identity.privateKey.item,
|
|
1451
|
+
...identity.privateKey.account !== void 0 && {
|
|
1452
|
+
account: identity.privateKey.account
|
|
1453
|
+
},
|
|
1454
|
+
...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
|
|
1455
|
+
};
|
|
1456
|
+
} else {
|
|
1457
|
+
privateKey = identity.privateKey;
|
|
1458
|
+
}
|
|
1459
|
+
return [
|
|
1460
|
+
key,
|
|
1461
|
+
{
|
|
1462
|
+
name: identity.name,
|
|
1463
|
+
publicKey: identity.publicKey,
|
|
1464
|
+
privateKey,
|
|
1465
|
+
...identity.email !== void 0 && { email: identity.email },
|
|
1466
|
+
...identity.github !== void 0 && { github: identity.github }
|
|
1467
|
+
}
|
|
1468
|
+
];
|
|
1469
|
+
})
|
|
1470
|
+
);
|
|
1471
|
+
return {
|
|
1472
|
+
activeIdentity: result.data.activeIdentity,
|
|
1473
|
+
identities
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
async function loadLocalConfig(configPath) {
|
|
1477
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1478
|
+
try {
|
|
1479
|
+
const content = await readFile(resolvedPath, "utf8");
|
|
1480
|
+
return parseLocalConfigContent(content);
|
|
1481
|
+
} catch (error) {
|
|
1482
|
+
if (error instanceof LocalConfigValidationError) {
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
1485
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
throw error;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function loadLocalConfigSync(configPath) {
|
|
1492
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1493
|
+
try {
|
|
1494
|
+
const content = readFileSync(resolvedPath, "utf8");
|
|
1495
|
+
return parseLocalConfigContent(content);
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
if (error instanceof LocalConfigValidationError) {
|
|
1498
|
+
throw error;
|
|
1499
|
+
}
|
|
1500
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
1501
|
+
return null;
|
|
1502
|
+
}
|
|
1503
|
+
throw error;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
async function saveLocalConfig(config, configPath) {
|
|
1507
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1508
|
+
const content = stringify(config);
|
|
1509
|
+
const dir = dirname(resolvedPath);
|
|
1510
|
+
await mkdir(dir, { recursive: true });
|
|
1511
|
+
await writeFile(resolvedPath, content, "utf8");
|
|
1512
|
+
}
|
|
1513
|
+
function saveLocalConfigSync(config, configPath) {
|
|
1514
|
+
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1515
|
+
const content = stringify(config);
|
|
1516
|
+
const dir = dirname(resolvedPath);
|
|
1517
|
+
mkdirSync(dir, { recursive: true });
|
|
1518
|
+
writeFileSync(resolvedPath, content, "utf8");
|
|
1519
|
+
}
|
|
1520
|
+
function getActiveIdentity(config) {
|
|
1521
|
+
return config.identities[config.activeIdentity];
|
|
1522
|
+
}
|
|
1523
|
+
function isAuthorizedSigner(config, gateId, publicKey) {
|
|
1524
|
+
const gate = config.gates?.[gateId];
|
|
1525
|
+
if (!gate) {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
const teamMember = findTeamMemberByPublicKey(config, publicKey);
|
|
1529
|
+
if (!teamMember) {
|
|
1530
|
+
return false;
|
|
1531
|
+
}
|
|
1532
|
+
const teamMemberSlug = findTeamMemberSlug(config, teamMember);
|
|
1533
|
+
if (!teamMemberSlug) {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
return gate.authorizedSigners.includes(teamMemberSlug);
|
|
1537
|
+
}
|
|
1538
|
+
function getAuthorizedSignersForGate(config, gateId) {
|
|
1539
|
+
const gate = config.gates?.[gateId];
|
|
1540
|
+
if (!gate || !config.team) {
|
|
1541
|
+
return [];
|
|
1542
|
+
}
|
|
1543
|
+
const authorizedMembers = [];
|
|
1544
|
+
for (const signerSlug of gate.authorizedSigners) {
|
|
1545
|
+
const member = config.team[signerSlug];
|
|
1546
|
+
if (member) {
|
|
1547
|
+
authorizedMembers.push(member);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return authorizedMembers;
|
|
1551
|
+
}
|
|
1552
|
+
function findTeamMemberByPublicKey(config, publicKey) {
|
|
1553
|
+
if (!config.team) {
|
|
1554
|
+
return void 0;
|
|
1555
|
+
}
|
|
1556
|
+
for (const member of Object.values(config.team)) {
|
|
1557
|
+
if (member.publicKey === publicKey) {
|
|
1558
|
+
return member;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return void 0;
|
|
1562
|
+
}
|
|
1563
|
+
function findTeamMemberSlug(config, teamMember) {
|
|
1564
|
+
if (!config.team) {
|
|
1565
|
+
return void 0;
|
|
1566
|
+
}
|
|
1567
|
+
for (const [slug, member] of Object.entries(config.team)) {
|
|
1568
|
+
if (member === teamMember || member.publicKey === teamMember.publicKey) {
|
|
1569
|
+
return slug;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return void 0;
|
|
1573
|
+
}
|
|
1574
|
+
function getGate(config, gateId) {
|
|
1575
|
+
return config.gates?.[gateId];
|
|
1576
|
+
}
|
|
1577
|
+
var DURATION_PATTERN = /^\d+(\.\d+)?\s*(ms|s|m|h|d|w|y)$/i;
|
|
1578
|
+
function isValidDurationFormat(value) {
|
|
1579
|
+
return DURATION_PATTERN.test(value.trim());
|
|
1580
|
+
}
|
|
1581
|
+
function parseDuration(duration) {
|
|
1582
|
+
if (!isValidDurationFormat(duration)) {
|
|
1583
|
+
throw new Error(`Invalid duration string: ${duration}`);
|
|
1584
|
+
}
|
|
1585
|
+
const result = ms(duration);
|
|
1586
|
+
if (typeof result !== "number" || result <= 0) {
|
|
1587
|
+
throw new Error(`Invalid duration string: ${duration}`);
|
|
1588
|
+
}
|
|
1589
|
+
return result;
|
|
1590
|
+
}
|
|
1591
|
+
var sealSchema = z.object({
|
|
1592
|
+
gateId: z.string().min(1, "Gate ID cannot be empty"),
|
|
1593
|
+
// Fingerprint format: sha256:<hex> where hex is at least 1 character
|
|
1594
|
+
// Full fingerprints are 64 hex chars, but tests may use shorter values
|
|
1595
|
+
fingerprint: z.string().regex(/^sha256:[a-f0-9]+$/i, "Invalid fingerprint format (expected sha256:<hex>)"),
|
|
1596
|
+
timestamp: z.string().datetime({ message: "Invalid ISO 8601 timestamp" }),
|
|
1597
|
+
sealedBy: z.string().min(1, "Signer slug cannot be empty"),
|
|
1598
|
+
signature: z.string().min(1, "Signature cannot be empty")
|
|
1599
|
+
});
|
|
1600
|
+
var sealsFileSchema = z.object({
|
|
1601
|
+
version: z.literal(1, { errorMap: () => ({ message: "Unsupported seals file version" }) }),
|
|
1602
|
+
seals: z.record(z.string(), sealSchema)
|
|
1603
|
+
});
|
|
1604
|
+
function createSeal(options) {
|
|
1605
|
+
const { gateId, fingerprint, sealedBy, privateKey } = options;
|
|
1606
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1607
|
+
const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
|
|
1608
|
+
const signature = sign3(canonicalString, privateKey);
|
|
1609
|
+
return {
|
|
1610
|
+
gateId,
|
|
1611
|
+
fingerprint,
|
|
1612
|
+
timestamp,
|
|
1613
|
+
sealedBy,
|
|
1614
|
+
signature
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function verifySeal(seal, config) {
|
|
1618
|
+
const { gateId, fingerprint, timestamp, sealedBy, signature } = seal;
|
|
1619
|
+
if (!config.team) {
|
|
1620
|
+
return {
|
|
1621
|
+
valid: false,
|
|
1622
|
+
error: `No team configuration found`
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
const teamMember = config.team[sealedBy];
|
|
1626
|
+
if (!teamMember) {
|
|
1627
|
+
return {
|
|
1628
|
+
valid: false,
|
|
1629
|
+
error: `Team member '${sealedBy}' not found in configuration`
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
const canonicalString = `${gateId}:${fingerprint}:${timestamp}`;
|
|
1633
|
+
try {
|
|
1634
|
+
const isValid = verify3(canonicalString, signature, teamMember.publicKey);
|
|
1635
|
+
if (!isValid) {
|
|
1636
|
+
return {
|
|
1637
|
+
valid: false,
|
|
1638
|
+
error: "Signature verification failed"
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
return { valid: true };
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
return {
|
|
1644
|
+
valid: false,
|
|
1645
|
+
error: `Signature verification error: ${error instanceof Error ? error.message : String(error)}`
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function parseSealsContent(content) {
|
|
1650
|
+
let rawData;
|
|
1651
|
+
try {
|
|
1652
|
+
rawData = JSON.parse(content);
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1655
|
+
}
|
|
1656
|
+
const result = sealsFileSchema.safeParse(rawData);
|
|
1657
|
+
if (!result.success) {
|
|
1658
|
+
const issues = result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n");
|
|
1659
|
+
throw new Error(`Invalid seals file:
|
|
1660
|
+
${issues}`);
|
|
1661
|
+
}
|
|
1662
|
+
return result.data;
|
|
1663
|
+
}
|
|
1664
|
+
async function readSeals(dir) {
|
|
1665
|
+
const sealsPath = path6.join(dir, ".attest-it", "seals.json");
|
|
1666
|
+
try {
|
|
1667
|
+
const content = await fs.promises.readFile(sealsPath, "utf8");
|
|
1668
|
+
return parseSealsContent(content);
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1671
|
+
return {
|
|
1672
|
+
version: 1,
|
|
1673
|
+
seals: {}
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
throw new Error(
|
|
1677
|
+
`Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function readSealsSync(dir) {
|
|
1682
|
+
const sealsPath = path6.join(dir, ".attest-it", "seals.json");
|
|
1683
|
+
try {
|
|
1684
|
+
const content = fs.readFileSync(sealsPath, "utf8");
|
|
1685
|
+
return parseSealsContent(content);
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1688
|
+
return {
|
|
1689
|
+
version: 1,
|
|
1690
|
+
seals: {}
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
`Failed to read seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
async function writeSeals(dir, sealsFile) {
|
|
1699
|
+
const attestItDir = path6.join(dir, ".attest-it");
|
|
1700
|
+
const sealsPath = path6.join(attestItDir, "seals.json");
|
|
1701
|
+
try {
|
|
1702
|
+
await fs.promises.mkdir(attestItDir, { recursive: true });
|
|
1703
|
+
const content = JSON.stringify(sealsFile, null, 2) + "\n";
|
|
1704
|
+
await fs.promises.writeFile(sealsPath, content, "utf8");
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
function writeSealsSync(dir, sealsFile) {
|
|
1712
|
+
const attestItDir = path6.join(dir, ".attest-it");
|
|
1713
|
+
const sealsPath = path6.join(attestItDir, "seals.json");
|
|
1714
|
+
try {
|
|
1715
|
+
fs.mkdirSync(attestItDir, { recursive: true });
|
|
1716
|
+
const content = JSON.stringify(sealsFile, null, 2) + "\n";
|
|
1717
|
+
fs.writeFileSync(sealsPath, content, "utf8");
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
throw new Error(
|
|
1720
|
+
`Failed to write seals file: ${error instanceof Error ? error.message : String(error)}`
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/seal/verification.ts
|
|
1726
|
+
function verifyGateSeal(config, gateId, seals, currentFingerprint) {
|
|
1727
|
+
const gate = getGate(config, gateId);
|
|
1728
|
+
if (!gate) {
|
|
1729
|
+
return {
|
|
1730
|
+
gateId,
|
|
1731
|
+
state: "MISSING",
|
|
1732
|
+
message: `Gate '${gateId}' not found in configuration`
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
const seal = seals.seals[gateId];
|
|
1736
|
+
if (!seal) {
|
|
1737
|
+
return {
|
|
1738
|
+
gateId,
|
|
1739
|
+
state: "MISSING",
|
|
1740
|
+
message: `No seal found for gate '${gateId}'`
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
if (seal.fingerprint !== currentFingerprint) {
|
|
1744
|
+
return {
|
|
1745
|
+
gateId,
|
|
1746
|
+
state: "FINGERPRINT_MISMATCH",
|
|
1747
|
+
seal,
|
|
1748
|
+
message: `Fingerprint changed since seal was created`
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
if (!config.team) {
|
|
1752
|
+
return {
|
|
1753
|
+
gateId,
|
|
1754
|
+
state: "UNKNOWN_SIGNER",
|
|
1755
|
+
seal,
|
|
1756
|
+
message: `No team configuration found`
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
const teamMember = config.team[seal.sealedBy];
|
|
1760
|
+
if (!teamMember) {
|
|
1761
|
+
return {
|
|
1762
|
+
gateId,
|
|
1763
|
+
state: "UNKNOWN_SIGNER",
|
|
1764
|
+
seal,
|
|
1765
|
+
message: `Signer '${seal.sealedBy}' not found in team`
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
const authorized = isAuthorizedSigner(config, gateId, teamMember.publicKey);
|
|
1769
|
+
if (!authorized) {
|
|
1770
|
+
return {
|
|
1771
|
+
gateId,
|
|
1772
|
+
state: "UNKNOWN_SIGNER",
|
|
1773
|
+
seal,
|
|
1774
|
+
message: `Signer '${seal.sealedBy}' is not authorized for gate '${gateId}'`
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
const verificationResult = verifySeal(seal, config);
|
|
1778
|
+
if (!verificationResult.valid) {
|
|
1779
|
+
return {
|
|
1780
|
+
gateId,
|
|
1781
|
+
state: "INVALID_SIGNATURE",
|
|
1782
|
+
seal,
|
|
1783
|
+
message: verificationResult.error ?? "Signature verification failed"
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
const maxAgeMs = parseDuration(gate.maxAge);
|
|
1788
|
+
const sealTimestamp = new Date(seal.timestamp).getTime();
|
|
1789
|
+
const now = Date.now();
|
|
1790
|
+
const ageMs = now - sealTimestamp;
|
|
1791
|
+
if (ageMs > maxAgeMs) {
|
|
1792
|
+
const ageDays = Math.floor(ageMs / (1e3 * 60 * 60 * 24));
|
|
1793
|
+
const maxAgeDays = Math.floor(maxAgeMs / (1e3 * 60 * 60 * 24));
|
|
1794
|
+
return {
|
|
1795
|
+
gateId,
|
|
1796
|
+
state: "STALE",
|
|
1797
|
+
seal,
|
|
1798
|
+
message: `Seal is ${ageDays.toString()} days old, exceeds maxAge of ${maxAgeDays.toString()} days`
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
return {
|
|
1803
|
+
gateId,
|
|
1804
|
+
state: "STALE",
|
|
1805
|
+
seal,
|
|
1806
|
+
message: `Cannot verify freshness: invalid maxAge format: ${error instanceof Error ? error.message : String(error)}`
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
return {
|
|
1810
|
+
gateId,
|
|
1811
|
+
state: "VALID",
|
|
1812
|
+
seal
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
function verifyAllSeals(config, seals, fingerprints) {
|
|
1816
|
+
if (!config.gates) {
|
|
1817
|
+
return [];
|
|
1818
|
+
}
|
|
1819
|
+
const results = [];
|
|
1820
|
+
for (const gateId of Object.keys(config.gates)) {
|
|
1821
|
+
const fingerprint = fingerprints[gateId];
|
|
1822
|
+
if (!fingerprint) {
|
|
1823
|
+
results.push({
|
|
1824
|
+
gateId,
|
|
1825
|
+
state: "MISSING",
|
|
1826
|
+
message: `No fingerprint computed for gate '${gateId}'`
|
|
1827
|
+
});
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
const result = verifyGateSeal(config, gateId, seals, fingerprint);
|
|
1831
|
+
results.push(result);
|
|
1832
|
+
}
|
|
1833
|
+
return results;
|
|
625
1834
|
}
|
|
626
1835
|
|
|
627
1836
|
// src/index.ts
|
|
628
1837
|
var version = "0.0.0";
|
|
629
1838
|
|
|
630
|
-
export { ConfigNotFoundError, ConfigValidationError, SignatureInvalidError, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, findAttestation, findConfigPath, listPackageFiles, loadConfig, loadConfigSync, readAndVerifyAttestations, readAttestations, readAttestationsSync, removeAttestation, resolveConfigPaths, toAttestItConfig, upsertAttestation, verifyAttestations, version, writeAttestations, writeAttestationsSync, writeSignedAttestations };
|
|
1839
|
+
export { ConfigNotFoundError, ConfigValidationError, FilesystemKeyProvider, KeyProviderRegistry, LocalConfigValidationError, MacOSKeychainKeyProvider, OnePasswordKeyProvider, SignatureInvalidError, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, createSeal, findAttestation, findConfigPath, findTeamMemberByPublicKey, generateKeyPair2 as generateEd25519KeyPair, getActiveIdentity, getAuthorizedSignersForGate, getGate, getLocalConfigPath, getPublicKeyFromPrivate, isAuthorizedSigner, listPackageFiles, loadConfig, loadConfigSync, loadLocalConfig, loadLocalConfigSync, parseDuration, readAndVerifyAttestations, readAttestations, readAttestationsSync, readSeals, readSealsSync, removeAttestation, resolveConfigPaths, saveLocalConfig, saveLocalConfigSync, sign3 as signEd25519, toAttestItConfig, upsertAttestation, verifyAllSeals, verifyAttestations, verify3 as verifyEd25519, verifyGateSeal, verifySeal, version, writeAttestations, writeAttestationsSync, writeSeals, writeSealsSync, writeSignedAttestations };
|
|
631
1840
|
//# sourceMappingURL=index.js.map
|
|
632
1841
|
//# sourceMappingURL=index.js.map
|