@attest-it/core 0.4.0 → 0.6.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/core-alpha.d.ts +1241 -523
- package/dist/core-beta.d.ts +1241 -523
- package/dist/core-public.d.ts +1241 -523
- package/dist/core-unstripped.d.ts +1241 -523
- package/dist/index.cjs +1057 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +791 -2
- package/dist/index.d.ts +787 -2
- package/dist/index.js +1019 -116
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@ import { getDefaultPrivateKeyPath, generateKeyPair, setKeyPermissions } from './
|
|
|
2
2
|
export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify } from './chunk-VC3BBBBO.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
5
|
-
import * as
|
|
5
|
+
import * as fs7 from 'fs/promises';
|
|
6
6
|
import { readFile, mkdir, writeFile } from 'fs/promises';
|
|
7
7
|
import * as path6 from 'path';
|
|
8
8
|
import { join, resolve, dirname } from 'path';
|
|
9
9
|
import ms from 'ms';
|
|
10
|
-
import {
|
|
10
|
+
import { parse, stringify } from 'yaml';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import * as
|
|
12
|
+
import * as crypto3 from 'crypto';
|
|
13
13
|
import { glob, globSync } from 'tinyglobby';
|
|
14
14
|
import * as os2 from 'os';
|
|
15
15
|
import { homedir } from 'os';
|
|
@@ -255,6 +255,299 @@ function toAttestItConfig(config) {
|
|
|
255
255
|
);
|
|
256
256
|
return result;
|
|
257
257
|
}
|
|
258
|
+
var teamMemberSchema2 = z.object({
|
|
259
|
+
name: z.string().min(1, "Team member name cannot be empty"),
|
|
260
|
+
email: z.string().email().optional(),
|
|
261
|
+
github: z.string().min(1).optional(),
|
|
262
|
+
publicKey: z.string().min(1, "Public key is required")
|
|
263
|
+
}).strict();
|
|
264
|
+
var fingerprintConfigSchema2 = z.object({
|
|
265
|
+
paths: z.array(z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
|
|
266
|
+
exclude: z.array(z.string().min(1, "Exclude pattern cannot be empty")).optional()
|
|
267
|
+
}).strict();
|
|
268
|
+
var durationSchema2 = z.string().refine(
|
|
269
|
+
(val) => {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = ms(val);
|
|
272
|
+
return typeof parsed === "number" && parsed > 0;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
var gateSchema2 = z.object({
|
|
282
|
+
name: z.string().min(1, "Gate name cannot be empty"),
|
|
283
|
+
description: z.string().min(1, "Gate description cannot be empty"),
|
|
284
|
+
authorizedSigners: z.array(z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
|
|
285
|
+
fingerprint: fingerprintConfigSchema2,
|
|
286
|
+
maxAge: durationSchema2
|
|
287
|
+
}).strict();
|
|
288
|
+
var keyProviderOptionsSchema2 = z.object({
|
|
289
|
+
privateKeyPath: z.string().optional(),
|
|
290
|
+
account: z.string().optional(),
|
|
291
|
+
vault: z.string().optional(),
|
|
292
|
+
itemName: z.string().optional()
|
|
293
|
+
}).passthrough();
|
|
294
|
+
var keyProviderSchema2 = z.object({
|
|
295
|
+
type: z.enum(["filesystem", "1password"]).or(z.string()),
|
|
296
|
+
options: keyProviderOptionsSchema2.optional()
|
|
297
|
+
}).strict();
|
|
298
|
+
|
|
299
|
+
// src/config/policy-schema.ts
|
|
300
|
+
var policySettingsSchema = z.object({
|
|
301
|
+
maxAgeDays: z.number().int().positive().default(30),
|
|
302
|
+
publicKeyPath: z.string().default(".attest-it/pubkey.pem"),
|
|
303
|
+
attestationsPath: z.string().default(".attest-it/attestations.json")
|
|
304
|
+
}).strict();
|
|
305
|
+
var policySchema = z.object({
|
|
306
|
+
version: z.literal(1),
|
|
307
|
+
settings: policySettingsSchema.default({}),
|
|
308
|
+
team: z.record(z.string(), teamMemberSchema2).optional(),
|
|
309
|
+
gates: z.record(z.string(), gateSchema2).optional()
|
|
310
|
+
}).strict();
|
|
311
|
+
var PolicyValidationError = class extends Error {
|
|
312
|
+
constructor(message, issues) {
|
|
313
|
+
super(message);
|
|
314
|
+
this.issues = issues;
|
|
315
|
+
this.name = "PolicyValidationError";
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function parsePolicyContent(content, format) {
|
|
319
|
+
let rawConfig;
|
|
320
|
+
try {
|
|
321
|
+
if (format === "yaml") {
|
|
322
|
+
rawConfig = parse(content);
|
|
323
|
+
} else {
|
|
324
|
+
rawConfig = JSON.parse(content);
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new PolicyValidationError(
|
|
328
|
+
`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
|
|
329
|
+
[]
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const result = policySchema.safeParse(rawConfig);
|
|
333
|
+
if (!result.success) {
|
|
334
|
+
throw new PolicyValidationError(
|
|
335
|
+
"Policy validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
336
|
+
result.error.issues
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return result.data;
|
|
340
|
+
}
|
|
341
|
+
var operationalSettingsSchema = z.object({
|
|
342
|
+
defaultCommand: z.string().optional(),
|
|
343
|
+
keyProvider: keyProviderSchema2.optional()
|
|
344
|
+
}).strict();
|
|
345
|
+
var suiteSchema2 = z.object({
|
|
346
|
+
// Gate fields (if present, this suite references a gate)
|
|
347
|
+
gate: z.string().optional(),
|
|
348
|
+
// Legacy fingerprint definition (for backward compatibility)
|
|
349
|
+
description: z.string().optional(),
|
|
350
|
+
packages: z.array(z.string().min(1, "Package path cannot be empty")).optional(),
|
|
351
|
+
files: z.array(z.string().min(1, "File path cannot be empty")).optional(),
|
|
352
|
+
ignore: z.array(z.string().min(1, "Ignore pattern cannot be empty")).optional(),
|
|
353
|
+
// CLI-specific fields
|
|
354
|
+
command: z.string().optional(),
|
|
355
|
+
timeout: z.string().optional(),
|
|
356
|
+
interactive: z.boolean().optional(),
|
|
357
|
+
// Relationship fields
|
|
358
|
+
invalidates: z.array(z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
|
|
359
|
+
depends_on: z.array(z.string().min(1, "Dependency suite name cannot be empty")).optional()
|
|
360
|
+
}).strict().refine(
|
|
361
|
+
(suite) => {
|
|
362
|
+
return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
message: "Suite must either reference a gate or define packages for fingerprinting"
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
var operationalSchema = z.object({
|
|
369
|
+
version: z.literal(1),
|
|
370
|
+
settings: operationalSettingsSchema.default({}),
|
|
371
|
+
suites: z.record(z.string(), suiteSchema2).refine((suites) => Object.keys(suites).length >= 1, {
|
|
372
|
+
message: "At least one suite must be defined"
|
|
373
|
+
}),
|
|
374
|
+
groups: z.record(z.string(), z.array(z.string().min(1, "Suite name in group cannot be empty"))).optional()
|
|
375
|
+
}).strict();
|
|
376
|
+
var OperationalValidationError = class extends Error {
|
|
377
|
+
constructor(message, issues) {
|
|
378
|
+
super(message);
|
|
379
|
+
this.issues = issues;
|
|
380
|
+
this.name = "OperationalValidationError";
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function parseOperationalContent(content, format) {
|
|
384
|
+
let rawConfig;
|
|
385
|
+
try {
|
|
386
|
+
if (format === "yaml") {
|
|
387
|
+
rawConfig = parse(content);
|
|
388
|
+
} else {
|
|
389
|
+
rawConfig = JSON.parse(content);
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
throw new OperationalValidationError(
|
|
393
|
+
`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
|
|
394
|
+
[]
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
const result = operationalSchema.safeParse(rawConfig);
|
|
398
|
+
if (!result.success) {
|
|
399
|
+
throw new OperationalValidationError(
|
|
400
|
+
"Operational configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
401
|
+
result.error.issues
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return result.data;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/config/merge.ts
|
|
408
|
+
function toSuiteConfig(suite) {
|
|
409
|
+
const result = {};
|
|
410
|
+
if (suite.gate !== void 0) result.gate = suite.gate;
|
|
411
|
+
if (suite.description !== void 0) result.description = suite.description;
|
|
412
|
+
if (suite.packages !== void 0) result.packages = suite.packages;
|
|
413
|
+
if (suite.files !== void 0) result.files = suite.files;
|
|
414
|
+
if (suite.ignore !== void 0) result.ignore = suite.ignore;
|
|
415
|
+
if (suite.command !== void 0) result.command = suite.command;
|
|
416
|
+
if (suite.timeout !== void 0) result.timeout = suite.timeout;
|
|
417
|
+
if (suite.interactive !== void 0) result.interactive = suite.interactive;
|
|
418
|
+
if (suite.invalidates !== void 0) result.invalidates = suite.invalidates;
|
|
419
|
+
if (suite.depends_on !== void 0) result.depends_on = suite.depends_on;
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
function toTeamMember(member) {
|
|
423
|
+
const result = {
|
|
424
|
+
name: member.name,
|
|
425
|
+
publicKey: member.publicKey
|
|
426
|
+
};
|
|
427
|
+
if (member.email !== void 0) result.email = member.email;
|
|
428
|
+
if (member.github !== void 0) result.github = member.github;
|
|
429
|
+
return result;
|
|
430
|
+
}
|
|
431
|
+
function toGateConfig(gate) {
|
|
432
|
+
const fingerprint = {
|
|
433
|
+
paths: gate.fingerprint.paths
|
|
434
|
+
};
|
|
435
|
+
if (gate.fingerprint.exclude !== void 0) {
|
|
436
|
+
fingerprint.exclude = gate.fingerprint.exclude;
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
name: gate.name,
|
|
440
|
+
description: gate.description,
|
|
441
|
+
authorizedSigners: gate.authorizedSigners,
|
|
442
|
+
fingerprint,
|
|
443
|
+
maxAge: gate.maxAge
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function toKeyProvider(provider) {
|
|
447
|
+
const result = {
|
|
448
|
+
type: provider.type
|
|
449
|
+
};
|
|
450
|
+
if (provider.options !== void 0) {
|
|
451
|
+
const options = {};
|
|
452
|
+
let hasOptions = false;
|
|
453
|
+
if (provider.options.privateKeyPath !== void 0) {
|
|
454
|
+
options.privateKeyPath = provider.options.privateKeyPath;
|
|
455
|
+
hasOptions = true;
|
|
456
|
+
}
|
|
457
|
+
if (provider.options.account !== void 0) {
|
|
458
|
+
options.account = provider.options.account;
|
|
459
|
+
hasOptions = true;
|
|
460
|
+
}
|
|
461
|
+
if (provider.options.vault !== void 0) {
|
|
462
|
+
options.vault = provider.options.vault;
|
|
463
|
+
hasOptions = true;
|
|
464
|
+
}
|
|
465
|
+
if (provider.options.itemName !== void 0) {
|
|
466
|
+
options.itemName = provider.options.itemName;
|
|
467
|
+
hasOptions = true;
|
|
468
|
+
}
|
|
469
|
+
if (hasOptions) {
|
|
470
|
+
result.options = options;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
function mergeConfigs(policy, operational) {
|
|
476
|
+
const settings = {
|
|
477
|
+
// Security settings from policy (these are trust-critical)
|
|
478
|
+
maxAgeDays: policy.settings.maxAgeDays,
|
|
479
|
+
publicKeyPath: policy.settings.publicKeyPath,
|
|
480
|
+
attestationsPath: policy.settings.attestationsPath
|
|
481
|
+
};
|
|
482
|
+
if (operational.settings.defaultCommand !== void 0) {
|
|
483
|
+
settings.defaultCommand = operational.settings.defaultCommand;
|
|
484
|
+
}
|
|
485
|
+
if (operational.settings.keyProvider !== void 0) {
|
|
486
|
+
settings.keyProvider = toKeyProvider(operational.settings.keyProvider);
|
|
487
|
+
}
|
|
488
|
+
const suites = {};
|
|
489
|
+
for (const [name, suite] of Object.entries(operational.suites)) {
|
|
490
|
+
suites[name] = toSuiteConfig(suite);
|
|
491
|
+
}
|
|
492
|
+
const config = {
|
|
493
|
+
version: 1,
|
|
494
|
+
settings,
|
|
495
|
+
suites
|
|
496
|
+
};
|
|
497
|
+
if (policy.team !== void 0) {
|
|
498
|
+
const team = {};
|
|
499
|
+
for (const [slug, member] of Object.entries(policy.team)) {
|
|
500
|
+
team[slug] = toTeamMember(member);
|
|
501
|
+
}
|
|
502
|
+
config.team = team;
|
|
503
|
+
}
|
|
504
|
+
if (policy.gates !== void 0) {
|
|
505
|
+
const gates = {};
|
|
506
|
+
for (const [slug, gate] of Object.entries(policy.gates)) {
|
|
507
|
+
gates[slug] = toGateConfig(gate);
|
|
508
|
+
}
|
|
509
|
+
config.gates = gates;
|
|
510
|
+
}
|
|
511
|
+
if (operational.groups !== void 0) {
|
|
512
|
+
config.groups = operational.groups;
|
|
513
|
+
}
|
|
514
|
+
return config;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/config/validation.ts
|
|
518
|
+
function validateSuiteGateReferences(policy, operational) {
|
|
519
|
+
const errors = [];
|
|
520
|
+
const gates = policy.gates ?? {};
|
|
521
|
+
const team = policy.team ?? {};
|
|
522
|
+
for (const [suiteName, suiteConfig] of Object.entries(operational.suites)) {
|
|
523
|
+
const gateName = suiteConfig.gate;
|
|
524
|
+
if (gateName === void 0) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const gate = gates[gateName];
|
|
528
|
+
if (gate === void 0) {
|
|
529
|
+
errors.push({
|
|
530
|
+
type: "UNKNOWN_GATE",
|
|
531
|
+
suite: suiteName,
|
|
532
|
+
gate: gateName,
|
|
533
|
+
message: `Suite "${suiteName}" references unknown gate "${gateName}". The gate must be defined in policy.yaml.`
|
|
534
|
+
});
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
for (const signerSlug of gate.authorizedSigners) {
|
|
538
|
+
if (team[signerSlug] === void 0) {
|
|
539
|
+
errors.push({
|
|
540
|
+
type: "MISSING_TEAM_MEMBER",
|
|
541
|
+
suite: suiteName,
|
|
542
|
+
gate: gateName,
|
|
543
|
+
signer: signerSlug,
|
|
544
|
+
message: `Gate "${gateName}" (referenced by suite "${suiteName}") authorizes signer "${signerSlug}", but this team member is not defined in policy.yaml.`
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return errors;
|
|
550
|
+
}
|
|
258
551
|
var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
|
|
259
552
|
function sortFiles(files) {
|
|
260
553
|
return [...files].sort((a, b) => {
|
|
@@ -274,13 +567,13 @@ function computeFinalFingerprint(fileHashes) {
|
|
|
274
567
|
});
|
|
275
568
|
const hashes = sorted.map((input) => input.hash);
|
|
276
569
|
const concatenated = Buffer.concat(hashes);
|
|
277
|
-
const finalHash =
|
|
570
|
+
const finalHash = crypto3.createHash("sha256").update(concatenated).digest();
|
|
278
571
|
return `sha256:${finalHash.toString("hex")}`;
|
|
279
572
|
}
|
|
280
573
|
async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
281
574
|
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
282
|
-
return new Promise((
|
|
283
|
-
const hash2 =
|
|
575
|
+
return new Promise((resolve4, reject) => {
|
|
576
|
+
const hash2 = crypto3.createHash("sha256");
|
|
284
577
|
hash2.update(normalizedPath);
|
|
285
578
|
hash2.update(":");
|
|
286
579
|
const stream = fs.createReadStream(realPath);
|
|
@@ -288,13 +581,13 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
288
581
|
hash2.update(chunk);
|
|
289
582
|
});
|
|
290
583
|
stream.on("end", () => {
|
|
291
|
-
|
|
584
|
+
resolve4(hash2.digest());
|
|
292
585
|
});
|
|
293
586
|
stream.on("error", reject);
|
|
294
587
|
});
|
|
295
588
|
}
|
|
296
589
|
const content = await fs.promises.readFile(realPath);
|
|
297
|
-
const hash =
|
|
590
|
+
const hash = crypto3.createHash("sha256");
|
|
298
591
|
hash.update(normalizedPath);
|
|
299
592
|
hash.update(":");
|
|
300
593
|
hash.update(content);
|
|
@@ -302,7 +595,7 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
302
595
|
}
|
|
303
596
|
function hashFileSync(realPath, normalizedPath) {
|
|
304
597
|
const content = fs.readFileSync(realPath);
|
|
305
|
-
const hash =
|
|
598
|
+
const hash = crypto3.createHash("sha256");
|
|
306
599
|
hash.update(normalizedPath);
|
|
307
600
|
hash.update(":");
|
|
308
601
|
hash.update(content);
|
|
@@ -607,7 +900,7 @@ function isBuffer(value) {
|
|
|
607
900
|
}
|
|
608
901
|
function generateKeyPair2() {
|
|
609
902
|
try {
|
|
610
|
-
const keyPair =
|
|
903
|
+
const keyPair = crypto3.generateKeyPairSync("ed25519", {
|
|
611
904
|
publicKeyEncoding: {
|
|
612
905
|
type: "spki",
|
|
613
906
|
format: "pem"
|
|
@@ -621,7 +914,7 @@ function generateKeyPair2() {
|
|
|
621
914
|
if (typeof publicKey !== "string" || typeof privateKey !== "string") {
|
|
622
915
|
throw new Error("Expected keypair to have string keys");
|
|
623
916
|
}
|
|
624
|
-
const publicKeyObj =
|
|
917
|
+
const publicKeyObj = crypto3.createPublicKey(publicKey);
|
|
625
918
|
const publicKeyExport = publicKeyObj.export({
|
|
626
919
|
type: "spki",
|
|
627
920
|
format: "der"
|
|
@@ -644,8 +937,8 @@ function generateKeyPair2() {
|
|
|
644
937
|
function sign3(data, privateKeyPem) {
|
|
645
938
|
try {
|
|
646
939
|
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
647
|
-
const privateKeyObj =
|
|
648
|
-
const signatureResult =
|
|
940
|
+
const privateKeyObj = crypto3.createPrivateKey(privateKeyPem);
|
|
941
|
+
const signatureResult = crypto3.sign(null, dataBuffer, privateKeyObj);
|
|
649
942
|
if (!isBuffer(signatureResult)) {
|
|
650
943
|
throw new Error("Expected signature to be a Buffer");
|
|
651
944
|
}
|
|
@@ -685,12 +978,12 @@ function verify3(data, signature, publicKeyBase64) {
|
|
|
685
978
|
// BIT STRING, 33 bytes (32 key + 1 padding)
|
|
686
979
|
]);
|
|
687
980
|
const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
|
|
688
|
-
const publicKeyObj =
|
|
981
|
+
const publicKeyObj = crypto3.createPublicKey({
|
|
689
982
|
key: spkiBuffer,
|
|
690
983
|
format: "der",
|
|
691
984
|
type: "spki"
|
|
692
985
|
});
|
|
693
|
-
return
|
|
986
|
+
return crypto3.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
|
|
694
987
|
} catch (err) {
|
|
695
988
|
if (err instanceof Error && err.message.includes("verification failed")) {
|
|
696
989
|
return false;
|
|
@@ -702,8 +995,8 @@ function verify3(data, signature, publicKeyBase64) {
|
|
|
702
995
|
}
|
|
703
996
|
function getPublicKeyFromPrivate(privateKeyPem) {
|
|
704
997
|
try {
|
|
705
|
-
const privateKeyObj =
|
|
706
|
-
const publicKeyObj =
|
|
998
|
+
const privateKeyObj = crypto3.createPrivateKey(privateKeyPem);
|
|
999
|
+
const publicKeyObj = crypto3.createPublicKey(privateKeyObj);
|
|
707
1000
|
const publicKeyExport = publicKeyObj.export({
|
|
708
1001
|
type: "spki",
|
|
709
1002
|
format: "der"
|
|
@@ -869,7 +1162,7 @@ var FilesystemKeyProvider = class {
|
|
|
869
1162
|
*/
|
|
870
1163
|
async keyExists(keyRef) {
|
|
871
1164
|
try {
|
|
872
|
-
await
|
|
1165
|
+
await fs7.access(keyRef);
|
|
873
1166
|
return true;
|
|
874
1167
|
} catch {
|
|
875
1168
|
return false;
|
|
@@ -1027,7 +1320,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1027
1320
|
`Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
|
|
1028
1321
|
);
|
|
1029
1322
|
}
|
|
1030
|
-
const tempDir = await
|
|
1323
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
1031
1324
|
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
1032
1325
|
try {
|
|
1033
1326
|
const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
|
|
@@ -1040,8 +1333,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1040
1333
|
keyPath: tempKeyPath,
|
|
1041
1334
|
cleanup: async () => {
|
|
1042
1335
|
try {
|
|
1043
|
-
await
|
|
1044
|
-
await
|
|
1336
|
+
await fs7.unlink(tempKeyPath);
|
|
1337
|
+
await fs7.rmdir(tempDir);
|
|
1045
1338
|
} catch (cleanupError) {
|
|
1046
1339
|
console.warn(
|
|
1047
1340
|
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1051,7 +1344,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1051
1344
|
};
|
|
1052
1345
|
} catch (error) {
|
|
1053
1346
|
try {
|
|
1054
|
-
await
|
|
1347
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1055
1348
|
} catch (cleanupError) {
|
|
1056
1349
|
console.warn(
|
|
1057
1350
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1067,7 +1360,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1067
1360
|
*/
|
|
1068
1361
|
async generateKeyPair(options) {
|
|
1069
1362
|
const { publicKeyPath, force = false } = options;
|
|
1070
|
-
const tempDir = await
|
|
1363
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
1071
1364
|
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
1072
1365
|
try {
|
|
1073
1366
|
await generateKeyPair({
|
|
@@ -1088,8 +1381,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1088
1381
|
args.push("--account", this.account);
|
|
1089
1382
|
}
|
|
1090
1383
|
await execCommand("op", args);
|
|
1091
|
-
await
|
|
1092
|
-
await
|
|
1384
|
+
await fs7.unlink(tempPrivateKeyPath);
|
|
1385
|
+
await fs7.rmdir(tempDir);
|
|
1093
1386
|
return {
|
|
1094
1387
|
privateKeyRef: this.itemName,
|
|
1095
1388
|
publicKeyPath,
|
|
@@ -1097,7 +1390,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1097
1390
|
};
|
|
1098
1391
|
} catch (error) {
|
|
1099
1392
|
try {
|
|
1100
|
-
await
|
|
1393
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1101
1394
|
} catch (cleanupError) {
|
|
1102
1395
|
console.warn(
|
|
1103
1396
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1121,7 +1414,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1121
1414
|
}
|
|
1122
1415
|
};
|
|
1123
1416
|
async function execCommand(command, args) {
|
|
1124
|
-
return new Promise((
|
|
1417
|
+
return new Promise((resolve4, reject) => {
|
|
1125
1418
|
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1126
1419
|
let stdout = "";
|
|
1127
1420
|
let stderr = "";
|
|
@@ -1133,7 +1426,7 @@ async function execCommand(command, args) {
|
|
|
1133
1426
|
});
|
|
1134
1427
|
proc.on("close", (code) => {
|
|
1135
1428
|
if (code === 0) {
|
|
1136
|
-
|
|
1429
|
+
resolve4(stdout.trim());
|
|
1137
1430
|
} else {
|
|
1138
1431
|
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1139
1432
|
}
|
|
@@ -1147,6 +1440,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1147
1440
|
type = "macos-keychain";
|
|
1148
1441
|
displayName = "macOS Keychain";
|
|
1149
1442
|
itemName;
|
|
1443
|
+
keychain;
|
|
1150
1444
|
static ACCOUNT = "attest-it";
|
|
1151
1445
|
/**
|
|
1152
1446
|
* Create a new MacOSKeychainKeyProvider.
|
|
@@ -1154,6 +1448,9 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1154
1448
|
*/
|
|
1155
1449
|
constructor(options) {
|
|
1156
1450
|
this.itemName = options.itemName;
|
|
1451
|
+
if (options.keychain !== void 0) {
|
|
1452
|
+
this.keychain = options.keychain;
|
|
1453
|
+
}
|
|
1157
1454
|
}
|
|
1158
1455
|
/**
|
|
1159
1456
|
* Check if this provider is available.
|
|
@@ -1162,6 +1459,32 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1162
1459
|
static isAvailable() {
|
|
1163
1460
|
return process.platform === "darwin";
|
|
1164
1461
|
}
|
|
1462
|
+
/**
|
|
1463
|
+
* List available keychains on the system.
|
|
1464
|
+
* @returns Array of keychain information
|
|
1465
|
+
*/
|
|
1466
|
+
static async listKeychains() {
|
|
1467
|
+
if (!_MacOSKeychainKeyProvider.isAvailable()) {
|
|
1468
|
+
return [];
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
const output = await execCommand2("security", ["list-keychains"]);
|
|
1472
|
+
const keychains = [];
|
|
1473
|
+
const lines = output.split("\n");
|
|
1474
|
+
for (const line of lines) {
|
|
1475
|
+
const match = /"(.+)"/.exec(line.trim());
|
|
1476
|
+
if (match?.[1]) {
|
|
1477
|
+
const fullPath = match[1];
|
|
1478
|
+
const filename = fullPath.split("/").pop() ?? fullPath;
|
|
1479
|
+
const name = filename.replace(/\.keychain(-db)?$/, "");
|
|
1480
|
+
keychains.push({ path: fullPath, name });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return keychains;
|
|
1484
|
+
} catch {
|
|
1485
|
+
return [];
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1165
1488
|
/**
|
|
1166
1489
|
* Check if this provider is available on the current system.
|
|
1167
1490
|
*/
|
|
@@ -1174,13 +1497,11 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1174
1497
|
*/
|
|
1175
1498
|
async keyExists(keyRef) {
|
|
1176
1499
|
try {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
keyRef
|
|
1183
|
-
]);
|
|
1500
|
+
const args = ["find-generic-password", "-a", _MacOSKeychainKeyProvider.ACCOUNT, "-s", keyRef];
|
|
1501
|
+
if (this.keychain) {
|
|
1502
|
+
args.push(this.keychain);
|
|
1503
|
+
}
|
|
1504
|
+
await execCommand2("security", args);
|
|
1184
1505
|
return true;
|
|
1185
1506
|
} catch {
|
|
1186
1507
|
return false;
|
|
@@ -1198,26 +1519,30 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1198
1519
|
`Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
|
|
1199
1520
|
);
|
|
1200
1521
|
}
|
|
1201
|
-
const tempDir = await
|
|
1522
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
1202
1523
|
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
1203
1524
|
try {
|
|
1204
|
-
const
|
|
1525
|
+
const findArgs = [
|
|
1205
1526
|
"find-generic-password",
|
|
1206
1527
|
"-a",
|
|
1207
1528
|
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1208
1529
|
"-s",
|
|
1209
1530
|
keyRef,
|
|
1210
1531
|
"-w"
|
|
1211
|
-
]
|
|
1532
|
+
];
|
|
1533
|
+
if (this.keychain) {
|
|
1534
|
+
findArgs.push(this.keychain);
|
|
1535
|
+
}
|
|
1536
|
+
const base64Key = await execCommand2("security", findArgs);
|
|
1212
1537
|
const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
|
|
1213
|
-
await
|
|
1538
|
+
await fs7.writeFile(tempKeyPath, keyContent, { mode: 384 });
|
|
1214
1539
|
await setKeyPermissions(tempKeyPath);
|
|
1215
1540
|
return {
|
|
1216
1541
|
keyPath: tempKeyPath,
|
|
1217
1542
|
cleanup: async () => {
|
|
1218
1543
|
try {
|
|
1219
|
-
await
|
|
1220
|
-
await
|
|
1544
|
+
await fs7.unlink(tempKeyPath);
|
|
1545
|
+
await fs7.rmdir(tempDir);
|
|
1221
1546
|
} catch (cleanupError) {
|
|
1222
1547
|
console.warn(
|
|
1223
1548
|
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1227,7 +1552,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1227
1552
|
};
|
|
1228
1553
|
} catch (error) {
|
|
1229
1554
|
try {
|
|
1230
|
-
await
|
|
1555
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1231
1556
|
} catch (cleanupError) {
|
|
1232
1557
|
console.warn(
|
|
1233
1558
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1243,7 +1568,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1243
1568
|
*/
|
|
1244
1569
|
async generateKeyPair(options) {
|
|
1245
1570
|
const { publicKeyPath, force = false } = options;
|
|
1246
|
-
const tempDir = await
|
|
1571
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
1247
1572
|
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
1248
1573
|
try {
|
|
1249
1574
|
await generateKeyPair({
|
|
@@ -1251,9 +1576,9 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1251
1576
|
publicPath: publicKeyPath,
|
|
1252
1577
|
force
|
|
1253
1578
|
});
|
|
1254
|
-
const privateKeyContent = await
|
|
1579
|
+
const privateKeyContent = await fs7.readFile(tempPrivateKeyPath, "utf8");
|
|
1255
1580
|
const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
|
|
1256
|
-
|
|
1581
|
+
const addArgs = [
|
|
1257
1582
|
"add-generic-password",
|
|
1258
1583
|
"-a",
|
|
1259
1584
|
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
@@ -1264,9 +1589,13 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1264
1589
|
"-T",
|
|
1265
1590
|
"",
|
|
1266
1591
|
"-U"
|
|
1267
|
-
]
|
|
1268
|
-
|
|
1269
|
-
|
|
1592
|
+
];
|
|
1593
|
+
if (this.keychain) {
|
|
1594
|
+
addArgs.push(this.keychain);
|
|
1595
|
+
}
|
|
1596
|
+
await execCommand2("security", addArgs);
|
|
1597
|
+
await fs7.unlink(tempPrivateKeyPath);
|
|
1598
|
+
await fs7.rmdir(tempDir);
|
|
1270
1599
|
return {
|
|
1271
1600
|
privateKeyRef: this.itemName,
|
|
1272
1601
|
publicKeyPath,
|
|
@@ -1274,7 +1603,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1274
1603
|
};
|
|
1275
1604
|
} catch (error) {
|
|
1276
1605
|
try {
|
|
1277
|
-
await
|
|
1606
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
1278
1607
|
} catch (cleanupError) {
|
|
1279
1608
|
console.warn(
|
|
1280
1609
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1296,7 +1625,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1296
1625
|
}
|
|
1297
1626
|
};
|
|
1298
1627
|
async function execCommand2(command, args) {
|
|
1299
|
-
return new Promise((
|
|
1628
|
+
return new Promise((resolve4, reject) => {
|
|
1300
1629
|
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1301
1630
|
let stdout = "";
|
|
1302
1631
|
let stderr = "";
|
|
@@ -1308,7 +1637,7 @@ async function execCommand2(command, args) {
|
|
|
1308
1637
|
});
|
|
1309
1638
|
proc.on("close", (code) => {
|
|
1310
1639
|
if (code === 0) {
|
|
1311
|
-
|
|
1640
|
+
resolve4(stdout.trim());
|
|
1312
1641
|
} else {
|
|
1313
1642
|
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1314
1643
|
}
|
|
@@ -1318,69 +1647,13 @@ async function execCommand2(command, args) {
|
|
|
1318
1647
|
});
|
|
1319
1648
|
});
|
|
1320
1649
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
});
|
|
1650
|
+
var homeDirOverride = null;
|
|
1651
|
+
function setAttestItHomeDir(dir) {
|
|
1652
|
+
homeDirOverride = dir;
|
|
1653
|
+
}
|
|
1654
|
+
function getAttestItHomeDir() {
|
|
1655
|
+
return homeDirOverride;
|
|
1656
|
+
}
|
|
1384
1657
|
var privateKeyRefSchema = z.discriminatedUnion("type", [
|
|
1385
1658
|
z.object({
|
|
1386
1659
|
type: z.literal("file"),
|
|
@@ -1389,7 +1662,8 @@ var privateKeyRefSchema = z.discriminatedUnion("type", [
|
|
|
1389
1662
|
z.object({
|
|
1390
1663
|
type: z.literal("keychain"),
|
|
1391
1664
|
service: z.string().min(1, "Service name cannot be empty"),
|
|
1392
|
-
account: z.string().min(1, "Account name cannot be empty")
|
|
1665
|
+
account: z.string().min(1, "Account name cannot be empty"),
|
|
1666
|
+
keychain: z.string().optional()
|
|
1393
1667
|
}),
|
|
1394
1668
|
z.object({
|
|
1395
1669
|
type: z.literal("1password"),
|
|
@@ -1420,9 +1694,18 @@ var LocalConfigValidationError = class extends Error {
|
|
|
1420
1694
|
}
|
|
1421
1695
|
};
|
|
1422
1696
|
function getLocalConfigPath() {
|
|
1697
|
+
if (homeDirOverride) {
|
|
1698
|
+
return join(homeDirOverride, "config.yaml");
|
|
1699
|
+
}
|
|
1423
1700
|
const home = homedir();
|
|
1424
1701
|
return join(home, ".config", "attest-it", "config.yaml");
|
|
1425
1702
|
}
|
|
1703
|
+
function getAttestItConfigDir() {
|
|
1704
|
+
if (homeDirOverride) {
|
|
1705
|
+
return homeDirOverride;
|
|
1706
|
+
}
|
|
1707
|
+
return join(homedir(), ".config", "attest-it");
|
|
1708
|
+
}
|
|
1426
1709
|
function parseLocalConfigContent(content) {
|
|
1427
1710
|
let rawConfig;
|
|
1428
1711
|
try {
|
|
@@ -1453,6 +1736,15 @@ function parseLocalConfigContent(content) {
|
|
|
1453
1736
|
},
|
|
1454
1737
|
...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
|
|
1455
1738
|
};
|
|
1739
|
+
} else if (identity.privateKey.type === "keychain") {
|
|
1740
|
+
privateKey = {
|
|
1741
|
+
type: "keychain",
|
|
1742
|
+
service: identity.privateKey.service,
|
|
1743
|
+
account: identity.privateKey.account,
|
|
1744
|
+
...identity.privateKey.keychain !== void 0 && {
|
|
1745
|
+
keychain: identity.privateKey.keychain
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1456
1748
|
} else {
|
|
1457
1749
|
privateKey = identity.privateKey;
|
|
1458
1750
|
}
|
|
@@ -1520,6 +1812,617 @@ function saveLocalConfigSync(config, configPath) {
|
|
|
1520
1812
|
function getActiveIdentity(config) {
|
|
1521
1813
|
return config.identities[config.activeIdentity];
|
|
1522
1814
|
}
|
|
1815
|
+
|
|
1816
|
+
// src/key-provider/yubikey-provider.ts
|
|
1817
|
+
var EncryptedKeyFileSchema = z.object({
|
|
1818
|
+
version: z.literal(1),
|
|
1819
|
+
iv: z.string().min(1),
|
|
1820
|
+
authTag: z.string().min(1),
|
|
1821
|
+
salt: z.string().min(1),
|
|
1822
|
+
challenge: z.string().min(1),
|
|
1823
|
+
ciphertext: z.string().min(1),
|
|
1824
|
+
slot: z.union([z.literal(1), z.literal(2)]),
|
|
1825
|
+
serial: z.string().optional(),
|
|
1826
|
+
aad: z.string().optional()
|
|
1827
|
+
});
|
|
1828
|
+
var activeCleanupHandlers = /* @__PURE__ */ new Set();
|
|
1829
|
+
var processHandlersInstalled = false;
|
|
1830
|
+
function installProcessHandlers() {
|
|
1831
|
+
if (processHandlersInstalled) return;
|
|
1832
|
+
processHandlersInstalled = true;
|
|
1833
|
+
const runCleanup = async () => {
|
|
1834
|
+
const handlers = Array.from(activeCleanupHandlers);
|
|
1835
|
+
await Promise.allSettled(handlers.map((h) => h()));
|
|
1836
|
+
};
|
|
1837
|
+
process.once("beforeExit", () => {
|
|
1838
|
+
void runCleanup();
|
|
1839
|
+
});
|
|
1840
|
+
process.once("SIGINT", () => {
|
|
1841
|
+
void runCleanup().finally(() => process.exit(130));
|
|
1842
|
+
});
|
|
1843
|
+
process.once("SIGTERM", () => {
|
|
1844
|
+
void runCleanup().finally(() => process.exit(143));
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
function validateEncryptedKeyFile(data) {
|
|
1848
|
+
const parsed = EncryptedKeyFileSchema.parse(data);
|
|
1849
|
+
const iv = Buffer.from(parsed.iv, "base64");
|
|
1850
|
+
if (iv.length !== 12) {
|
|
1851
|
+
throw new Error(`Invalid IV size: expected 12 bytes, got ${String(iv.length)}`);
|
|
1852
|
+
}
|
|
1853
|
+
const authTag = Buffer.from(parsed.authTag, "base64");
|
|
1854
|
+
if (authTag.length !== 16) {
|
|
1855
|
+
throw new Error(`Invalid auth tag size: expected 16 bytes, got ${String(authTag.length)}`);
|
|
1856
|
+
}
|
|
1857
|
+
const salt = Buffer.from(parsed.salt, "base64");
|
|
1858
|
+
if (salt.length !== 32) {
|
|
1859
|
+
throw new Error(`Invalid salt size: expected 32 bytes, got ${String(salt.length)}`);
|
|
1860
|
+
}
|
|
1861
|
+
const challenge = Buffer.from(parsed.challenge, "base64");
|
|
1862
|
+
if (challenge.length !== 32) {
|
|
1863
|
+
throw new Error(`Invalid challenge size: expected 32 bytes, got ${String(challenge.length)}`);
|
|
1864
|
+
}
|
|
1865
|
+
return parsed;
|
|
1866
|
+
}
|
|
1867
|
+
function constructAAD(version2, slot, serial) {
|
|
1868
|
+
const aadObject = {
|
|
1869
|
+
version: version2,
|
|
1870
|
+
slot,
|
|
1871
|
+
serial: serial ?? "unspecified"
|
|
1872
|
+
};
|
|
1873
|
+
return Buffer.from(JSON.stringify(aadObject), "utf8");
|
|
1874
|
+
}
|
|
1875
|
+
var YubiKeyProvider = class _YubiKeyProvider {
|
|
1876
|
+
type = "yubikey";
|
|
1877
|
+
displayName = "YubiKey";
|
|
1878
|
+
encryptedKeyPath;
|
|
1879
|
+
slot;
|
|
1880
|
+
serial;
|
|
1881
|
+
/**
|
|
1882
|
+
* Create a new YubiKeyProvider.
|
|
1883
|
+
* @param options - Provider options
|
|
1884
|
+
* @throws Error if encryptedKeyPath is outside the attest-it config directory
|
|
1885
|
+
*/
|
|
1886
|
+
constructor(options) {
|
|
1887
|
+
const resolvedPath = path6.resolve(options.encryptedKeyPath);
|
|
1888
|
+
const configDir = getAttestItConfigDir();
|
|
1889
|
+
if (!resolvedPath.startsWith(configDir)) {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
`Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
this.encryptedKeyPath = resolvedPath;
|
|
1895
|
+
this.slot = options.slot ?? 2;
|
|
1896
|
+
if (options.serial !== void 0) {
|
|
1897
|
+
this.serial = options.serial;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Check if ykman CLI is installed and available.
|
|
1902
|
+
* @returns true if ykman is available
|
|
1903
|
+
*/
|
|
1904
|
+
static async isInstalled() {
|
|
1905
|
+
try {
|
|
1906
|
+
await execCommand3("ykman", ["--version"]);
|
|
1907
|
+
return true;
|
|
1908
|
+
} catch {
|
|
1909
|
+
return false;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
/**
|
|
1913
|
+
* Check if any YubiKey is connected.
|
|
1914
|
+
* @returns true if at least one YubiKey is connected
|
|
1915
|
+
*/
|
|
1916
|
+
static async isConnected() {
|
|
1917
|
+
try {
|
|
1918
|
+
const output = await execCommand3("ykman", ["list", "--serials"]);
|
|
1919
|
+
return output.trim().length > 0;
|
|
1920
|
+
} catch {
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Check if HMAC challenge-response is configured on a slot.
|
|
1926
|
+
* @param slot - Slot number (1 or 2)
|
|
1927
|
+
* @param serial - Optional YubiKey serial number
|
|
1928
|
+
* @returns true if challenge-response is configured
|
|
1929
|
+
*/
|
|
1930
|
+
static async isChallengeResponseConfigured(slot = 2, serial) {
|
|
1931
|
+
try {
|
|
1932
|
+
const args = ["otp", "info"];
|
|
1933
|
+
if (serial) {
|
|
1934
|
+
args.unshift("--device", serial);
|
|
1935
|
+
}
|
|
1936
|
+
const output = await execCommand3("ykman", args);
|
|
1937
|
+
const slotPattern = new RegExp(`Slot ${String(slot)}:\\s+programmed.*challenge-response`, "i");
|
|
1938
|
+
return slotPattern.test(output);
|
|
1939
|
+
} catch {
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* List connected YubiKeys.
|
|
1945
|
+
* @returns Array of YubiKey information
|
|
1946
|
+
*/
|
|
1947
|
+
static async listDevices() {
|
|
1948
|
+
if (!await _YubiKeyProvider.isInstalled()) {
|
|
1949
|
+
return [];
|
|
1950
|
+
}
|
|
1951
|
+
try {
|
|
1952
|
+
const output = await execCommand3("ykman", ["list", "--serials"]);
|
|
1953
|
+
const serials = output.trim().split("\n").filter((s) => s.length > 0);
|
|
1954
|
+
const devices = [];
|
|
1955
|
+
for (const serial of serials) {
|
|
1956
|
+
try {
|
|
1957
|
+
const infoOutput = await execCommand3("ykman", ["--device", serial, "info"]);
|
|
1958
|
+
const typeMatch = /Device type:\s+(.+)/i.exec(infoOutput);
|
|
1959
|
+
const fwMatch = /Firmware version:\s+(.+)/i.exec(infoOutput);
|
|
1960
|
+
devices.push({
|
|
1961
|
+
serial,
|
|
1962
|
+
type: typeMatch?.[1]?.trim() ?? "YubiKey",
|
|
1963
|
+
firmware: fwMatch?.[1]?.trim() ?? "Unknown"
|
|
1964
|
+
});
|
|
1965
|
+
} catch {
|
|
1966
|
+
devices.push({
|
|
1967
|
+
serial,
|
|
1968
|
+
type: "YubiKey",
|
|
1969
|
+
firmware: "Unknown"
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
return devices;
|
|
1974
|
+
} catch {
|
|
1975
|
+
return [];
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Check if this provider is available on the current system.
|
|
1980
|
+
* Requires ykman to be installed.
|
|
1981
|
+
*/
|
|
1982
|
+
async isAvailable() {
|
|
1983
|
+
return _YubiKeyProvider.isInstalled();
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Check if an encrypted key file exists.
|
|
1987
|
+
* @param keyRef - Path to encrypted key file
|
|
1988
|
+
*/
|
|
1989
|
+
async keyExists(keyRef) {
|
|
1990
|
+
try {
|
|
1991
|
+
await fs7.access(keyRef);
|
|
1992
|
+
return true;
|
|
1993
|
+
} catch {
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Get the private key by decrypting with YubiKey.
|
|
1999
|
+
* Downloads to a temporary file and returns a cleanup function.
|
|
2000
|
+
*
|
|
2001
|
+
* **Important**: Always call the cleanup function when done to securely delete
|
|
2002
|
+
* the temporary key file. The cleanup is also registered for process exit handlers.
|
|
2003
|
+
*
|
|
2004
|
+
* @param keyRef - Path to encrypted key file
|
|
2005
|
+
* @throws Error if the key cannot be decrypted
|
|
2006
|
+
*/
|
|
2007
|
+
async getPrivateKey(keyRef) {
|
|
2008
|
+
installProcessHandlers();
|
|
2009
|
+
if (!await this.keyExists(keyRef)) {
|
|
2010
|
+
throw new Error(`Encrypted key file not found: ${keyRef}`);
|
|
2011
|
+
}
|
|
2012
|
+
const encryptedData = await fs7.readFile(keyRef, "utf8");
|
|
2013
|
+
let keyFile;
|
|
2014
|
+
try {
|
|
2015
|
+
const parsed = JSON.parse(encryptedData);
|
|
2016
|
+
keyFile = validateEncryptedKeyFile(parsed);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
if (err instanceof z.ZodError) {
|
|
2019
|
+
throw new Error(
|
|
2020
|
+
`Invalid encrypted key file format: ${err.errors.map((e) => e.message).join(", ")}`
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
throw new Error(`Invalid encrypted key file: malformed JSON or structure`);
|
|
2024
|
+
}
|
|
2025
|
+
const expectedSerial = this.serial ?? keyFile.serial;
|
|
2026
|
+
if (!expectedSerial) {
|
|
2027
|
+
console.warn(
|
|
2028
|
+
"WARNING: No YubiKey serial number specified for key verification. Any YubiKey with the correct HMAC secret could decrypt this key. For better security, re-encrypt the key with a serial number specified."
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
if (expectedSerial) {
|
|
2032
|
+
const devices = await _YubiKeyProvider.listDevices();
|
|
2033
|
+
const matchingDevice = devices.find((d) => d.serial === expectedSerial);
|
|
2034
|
+
if (!matchingDevice) {
|
|
2035
|
+
throw new Error(
|
|
2036
|
+
`Required YubiKey not found. Expected serial: ${expectedSerial}. Connected devices: ${devices.map((d) => d.serial).join(", ") || "none"}`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const challenge = Buffer.from(keyFile.challenge, "base64");
|
|
2041
|
+
const response = await performChallengeResponse(challenge, keyFile.slot, expectedSerial);
|
|
2042
|
+
const salt = Buffer.from(keyFile.salt, "base64");
|
|
2043
|
+
const aesKey = deriveKey(response, salt);
|
|
2044
|
+
const iv = Buffer.from(keyFile.iv, "base64");
|
|
2045
|
+
const authTag = Buffer.from(keyFile.authTag, "base64");
|
|
2046
|
+
const ciphertext = Buffer.from(keyFile.ciphertext, "base64");
|
|
2047
|
+
let privateKeyContent;
|
|
2048
|
+
try {
|
|
2049
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
2050
|
+
if (keyFile.aad) {
|
|
2051
|
+
decipher.setAAD(Buffer.from(keyFile.aad, "base64"));
|
|
2052
|
+
}
|
|
2053
|
+
decipher.setAuthTag(authTag);
|
|
2054
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
2055
|
+
privateKeyContent = decrypted.toString("utf8");
|
|
2056
|
+
} catch {
|
|
2057
|
+
throw new Error(
|
|
2058
|
+
"Failed to decrypt private key. Verify you are using the correct YubiKey and the encrypted key file has not been corrupted or tampered with."
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-"));
|
|
2062
|
+
const tempKeyPath = path6.join(tempDir, "private.pem");
|
|
2063
|
+
const cleanup = async () => {
|
|
2064
|
+
activeCleanupHandlers.delete(cleanup);
|
|
2065
|
+
try {
|
|
2066
|
+
const keySize = Buffer.byteLength(privateKeyContent);
|
|
2067
|
+
await fs7.writeFile(tempKeyPath, crypto3.randomBytes(keySize));
|
|
2068
|
+
await fs7.unlink(tempKeyPath);
|
|
2069
|
+
await fs7.rmdir(tempDir);
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
try {
|
|
2074
|
+
await fs7.writeFile(tempKeyPath, privateKeyContent, { mode: 384 });
|
|
2075
|
+
await setKeyPermissions(tempKeyPath);
|
|
2076
|
+
activeCleanupHandlers.add(cleanup);
|
|
2077
|
+
return {
|
|
2078
|
+
keyPath: tempKeyPath,
|
|
2079
|
+
cleanup
|
|
2080
|
+
};
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
await cleanup();
|
|
2083
|
+
throw error;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* Generate a new keypair and store encrypted with YubiKey.
|
|
2088
|
+
* Public key is written to filesystem for repository commit.
|
|
2089
|
+
*
|
|
2090
|
+
* **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
|
|
2091
|
+
*
|
|
2092
|
+
* @param options - Key generation options
|
|
2093
|
+
*/
|
|
2094
|
+
async generateKeyPair(options) {
|
|
2095
|
+
const { publicKeyPath, force = false } = options;
|
|
2096
|
+
if (!await _YubiKeyProvider.isChallengeResponseConfigured(this.slot, this.serial)) {
|
|
2097
|
+
throw new Error(
|
|
2098
|
+
`YubiKey slot ${String(this.slot)} is not configured for HMAC challenge-response. Ensure your YubiKey is connected and use "ykman otp chalresp --generate 2" to configure it.`
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
if (!force && await this.keyExists(this.encryptedKeyPath)) {
|
|
2102
|
+
throw new Error(
|
|
2103
|
+
`Encrypted key file already exists: ${this.encryptedKeyPath}. Use force: true to overwrite.`
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
let serial;
|
|
2107
|
+
if (this.serial) {
|
|
2108
|
+
serial = this.serial;
|
|
2109
|
+
} else {
|
|
2110
|
+
const devices = await _YubiKeyProvider.listDevices();
|
|
2111
|
+
if (devices.length === 1 && devices[0]) {
|
|
2112
|
+
serial = devices[0].serial;
|
|
2113
|
+
} else if (devices.length > 1) {
|
|
2114
|
+
console.warn(
|
|
2115
|
+
"WARNING: Multiple YubiKeys detected but no serial specified. Key will not be bound to a specific device. For better security, specify a serial number."
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
const tempDir = await fs7.mkdtemp(path6.join(os2.tmpdir(), "attest-it-keygen-"));
|
|
2120
|
+
const tempPrivateKeyPath = path6.join(tempDir, "private.pem");
|
|
2121
|
+
try {
|
|
2122
|
+
await generateKeyPair({
|
|
2123
|
+
privatePath: tempPrivateKeyPath,
|
|
2124
|
+
publicPath: publicKeyPath,
|
|
2125
|
+
force
|
|
2126
|
+
});
|
|
2127
|
+
const privateKeyContent = await fs7.readFile(tempPrivateKeyPath, "utf8");
|
|
2128
|
+
const challenge = crypto3.randomBytes(32);
|
|
2129
|
+
const salt = crypto3.randomBytes(32);
|
|
2130
|
+
const iv = crypto3.randomBytes(12);
|
|
2131
|
+
const response = await performChallengeResponse(challenge, this.slot, this.serial);
|
|
2132
|
+
const aesKey = deriveKey(response, salt);
|
|
2133
|
+
const aad = constructAAD(1, this.slot, serial);
|
|
2134
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
2135
|
+
cipher.setAAD(aad);
|
|
2136
|
+
const ciphertext = Buffer.concat([
|
|
2137
|
+
cipher.update(Buffer.from(privateKeyContent, "utf8")),
|
|
2138
|
+
cipher.final()
|
|
2139
|
+
]);
|
|
2140
|
+
const authTag = cipher.getAuthTag();
|
|
2141
|
+
const keyFile = {
|
|
2142
|
+
version: 1,
|
|
2143
|
+
iv: iv.toString("base64"),
|
|
2144
|
+
authTag: authTag.toString("base64"),
|
|
2145
|
+
salt: salt.toString("base64"),
|
|
2146
|
+
challenge: challenge.toString("base64"),
|
|
2147
|
+
ciphertext: ciphertext.toString("base64"),
|
|
2148
|
+
slot: this.slot,
|
|
2149
|
+
aad: aad.toString("base64"),
|
|
2150
|
+
...serial && { serial }
|
|
2151
|
+
};
|
|
2152
|
+
await fs7.mkdir(path6.dirname(this.encryptedKeyPath), { recursive: true });
|
|
2153
|
+
await fs7.writeFile(this.encryptedKeyPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
|
|
2154
|
+
await setKeyPermissions(this.encryptedKeyPath);
|
|
2155
|
+
const keySize = Buffer.byteLength(privateKeyContent);
|
|
2156
|
+
await fs7.writeFile(tempPrivateKeyPath, crypto3.randomBytes(keySize));
|
|
2157
|
+
await fs7.unlink(tempPrivateKeyPath);
|
|
2158
|
+
await fs7.rmdir(tempDir);
|
|
2159
|
+
return {
|
|
2160
|
+
privateKeyRef: this.encryptedKeyPath,
|
|
2161
|
+
publicKeyPath,
|
|
2162
|
+
storageDescription: `YubiKey-encrypted: ${this.encryptedKeyPath}`
|
|
2163
|
+
};
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
try {
|
|
2166
|
+
await fs7.rm(tempDir, { recursive: true, force: true });
|
|
2167
|
+
} catch {
|
|
2168
|
+
}
|
|
2169
|
+
throw error;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Encrypt an existing private key with YubiKey challenge-response.
|
|
2174
|
+
*
|
|
2175
|
+
* @remarks
|
|
2176
|
+
* This static method allows encrypting a private key that was generated
|
|
2177
|
+
* elsewhere (e.g., by the CLI) without having to create a provider instance first.
|
|
2178
|
+
*
|
|
2179
|
+
* **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
|
|
2180
|
+
* The serial provides defense-in-depth by ensuring only the intended YubiKey can decrypt.
|
|
2181
|
+
*
|
|
2182
|
+
* @param options - Encryption options
|
|
2183
|
+
* @returns Path to the encrypted key file and storage description
|
|
2184
|
+
* @public
|
|
2185
|
+
*/
|
|
2186
|
+
static async encryptPrivateKey(options) {
|
|
2187
|
+
const { privateKey, encryptedKeyPath, slot = 2, serial } = options;
|
|
2188
|
+
const resolvedPath = path6.resolve(encryptedKeyPath);
|
|
2189
|
+
const configDir = getAttestItConfigDir();
|
|
2190
|
+
if (!resolvedPath.startsWith(configDir)) {
|
|
2191
|
+
throw new Error(
|
|
2192
|
+
`Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
if (!serial) {
|
|
2196
|
+
console.warn(
|
|
2197
|
+
"WARNING: No YubiKey serial number specified. Key will not be bound to a specific device. For better security, specify a serial number."
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
if (!await _YubiKeyProvider.isChallengeResponseConfigured(slot, serial)) {
|
|
2201
|
+
throw new Error(
|
|
2202
|
+
`YubiKey slot ${String(slot)} is not configured for HMAC challenge-response. Ensure your YubiKey is connected and use "ykman otp chalresp --generate 2" to configure it.`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
const challenge = crypto3.randomBytes(32);
|
|
2206
|
+
const salt = crypto3.randomBytes(32);
|
|
2207
|
+
const iv = crypto3.randomBytes(12);
|
|
2208
|
+
const response = await performChallengeResponse(challenge, slot, serial);
|
|
2209
|
+
const aesKey = deriveKey(response, salt);
|
|
2210
|
+
const aad = constructAAD(1, slot, serial);
|
|
2211
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
2212
|
+
cipher.setAAD(aad);
|
|
2213
|
+
const ciphertext = Buffer.concat([
|
|
2214
|
+
cipher.update(Buffer.from(privateKey, "utf8")),
|
|
2215
|
+
cipher.final()
|
|
2216
|
+
]);
|
|
2217
|
+
const authTag = cipher.getAuthTag();
|
|
2218
|
+
const keyFile = {
|
|
2219
|
+
version: 1,
|
|
2220
|
+
iv: iv.toString("base64"),
|
|
2221
|
+
authTag: authTag.toString("base64"),
|
|
2222
|
+
salt: salt.toString("base64"),
|
|
2223
|
+
challenge: challenge.toString("base64"),
|
|
2224
|
+
ciphertext: ciphertext.toString("base64"),
|
|
2225
|
+
slot,
|
|
2226
|
+
aad: aad.toString("base64"),
|
|
2227
|
+
...serial && { serial }
|
|
2228
|
+
};
|
|
2229
|
+
await fs7.mkdir(path6.dirname(resolvedPath), { recursive: true });
|
|
2230
|
+
await fs7.writeFile(resolvedPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
|
|
2231
|
+
await setKeyPermissions(resolvedPath);
|
|
2232
|
+
return {
|
|
2233
|
+
encryptedKeyPath: resolvedPath,
|
|
2234
|
+
storageDescription: `YubiKey-encrypted: ${resolvedPath}`
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Get the configuration for this provider.
|
|
2239
|
+
*/
|
|
2240
|
+
getConfig() {
|
|
2241
|
+
return {
|
|
2242
|
+
type: this.type,
|
|
2243
|
+
options: {
|
|
2244
|
+
encryptedKeyPath: this.encryptedKeyPath,
|
|
2245
|
+
slot: this.slot,
|
|
2246
|
+
...this.serial && { serial: this.serial }
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
async function execCommand3(command, args) {
|
|
2252
|
+
return new Promise((resolve4, reject) => {
|
|
2253
|
+
const proc = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2254
|
+
let stdout = "";
|
|
2255
|
+
let stderr = "";
|
|
2256
|
+
proc.stdout.on("data", (data) => {
|
|
2257
|
+
stdout += data.toString();
|
|
2258
|
+
});
|
|
2259
|
+
proc.stderr.on("data", (data) => {
|
|
2260
|
+
stderr += data.toString();
|
|
2261
|
+
});
|
|
2262
|
+
proc.on("close", (code) => {
|
|
2263
|
+
if (code === 0) {
|
|
2264
|
+
resolve4(stdout.trim());
|
|
2265
|
+
} else {
|
|
2266
|
+
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
proc.on("error", (error) => {
|
|
2270
|
+
reject(error);
|
|
2271
|
+
});
|
|
2272
|
+
});
|
|
2273
|
+
}
|
|
2274
|
+
async function performChallengeResponse(challenge, slot, serial) {
|
|
2275
|
+
const args = ["otp", "chalresp", "--slot", String(slot)];
|
|
2276
|
+
if (serial) {
|
|
2277
|
+
args.unshift("--device", serial);
|
|
2278
|
+
}
|
|
2279
|
+
args.push(challenge.toString("hex"));
|
|
2280
|
+
try {
|
|
2281
|
+
const output = await execCommand3("ykman", args);
|
|
2282
|
+
return Buffer.from(output.trim(), "hex");
|
|
2283
|
+
} catch {
|
|
2284
|
+
throw new Error(
|
|
2285
|
+
"YubiKey challenge-response failed. Verify your YubiKey is inserted and the slot is configured for challenge-response."
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
function deriveKey(response, salt) {
|
|
2290
|
+
const derived = crypto3.hkdfSync("sha256", response, salt, "attest-it-yubikey-v1", 32);
|
|
2291
|
+
return Buffer.from(derived);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// src/key-provider/registry.ts
|
|
2295
|
+
var KeyProviderRegistry = class {
|
|
2296
|
+
static providers = /* @__PURE__ */ new Map();
|
|
2297
|
+
/**
|
|
2298
|
+
* Register a key provider factory.
|
|
2299
|
+
* @param type - Provider type identifier
|
|
2300
|
+
* @param factory - Factory function to create provider instances
|
|
2301
|
+
*/
|
|
2302
|
+
static register(type, factory) {
|
|
2303
|
+
this.providers.set(type, factory);
|
|
2304
|
+
}
|
|
2305
|
+
/**
|
|
2306
|
+
* Create a key provider from configuration.
|
|
2307
|
+
* @param config - Provider configuration
|
|
2308
|
+
* @returns A key provider instance
|
|
2309
|
+
* @throws Error if the provider type is not registered
|
|
2310
|
+
*/
|
|
2311
|
+
static create(config) {
|
|
2312
|
+
const factory = this.providers.get(config.type);
|
|
2313
|
+
if (!factory) {
|
|
2314
|
+
throw new Error(
|
|
2315
|
+
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
return factory(config);
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Get all registered provider types.
|
|
2322
|
+
* @returns Array of provider type identifiers
|
|
2323
|
+
*/
|
|
2324
|
+
static getProviderTypes() {
|
|
2325
|
+
return Array.from(this.providers.keys());
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
2328
|
+
KeyProviderRegistry.register("filesystem", (config) => {
|
|
2329
|
+
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
2330
|
+
if (privateKeyPath !== void 0) {
|
|
2331
|
+
return new FilesystemKeyProvider({ privateKeyPath });
|
|
2332
|
+
}
|
|
2333
|
+
return new FilesystemKeyProvider();
|
|
2334
|
+
});
|
|
2335
|
+
KeyProviderRegistry.register("1password", (config) => {
|
|
2336
|
+
const { options } = config;
|
|
2337
|
+
const account = typeof options.account === "string" ? options.account : void 0;
|
|
2338
|
+
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
2339
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
2340
|
+
if (!vault || !itemName) {
|
|
2341
|
+
throw new Error("1Password provider requires vault and itemName options");
|
|
2342
|
+
}
|
|
2343
|
+
if (account !== void 0) {
|
|
2344
|
+
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
2345
|
+
}
|
|
2346
|
+
return new OnePasswordKeyProvider({ vault, itemName });
|
|
2347
|
+
});
|
|
2348
|
+
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
2349
|
+
const { options } = config;
|
|
2350
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
2351
|
+
if (!itemName) {
|
|
2352
|
+
throw new Error("macOS Keychain provider requires itemName option");
|
|
2353
|
+
}
|
|
2354
|
+
return new MacOSKeychainKeyProvider({ itemName });
|
|
2355
|
+
});
|
|
2356
|
+
KeyProviderRegistry.register("yubikey", (config) => {
|
|
2357
|
+
const { options } = config;
|
|
2358
|
+
const encryptedKeyPath = typeof options.encryptedKeyPath === "string" ? options.encryptedKeyPath : "";
|
|
2359
|
+
if (!encryptedKeyPath) {
|
|
2360
|
+
throw new Error("YubiKey provider requires encryptedKeyPath option");
|
|
2361
|
+
}
|
|
2362
|
+
const slot = typeof options.slot === "number" && (options.slot === 1 || options.slot === 2) ? options.slot : void 0;
|
|
2363
|
+
const serial = typeof options.serial === "string" ? options.serial : void 0;
|
|
2364
|
+
const providerOptions = {
|
|
2365
|
+
encryptedKeyPath
|
|
2366
|
+
};
|
|
2367
|
+
if (slot !== void 0) {
|
|
2368
|
+
providerOptions.slot = slot;
|
|
2369
|
+
}
|
|
2370
|
+
if (serial !== void 0) {
|
|
2371
|
+
providerOptions.serial = serial;
|
|
2372
|
+
}
|
|
2373
|
+
return new YubiKeyProvider(providerOptions);
|
|
2374
|
+
});
|
|
2375
|
+
var cliExperienceSchema = z.object({
|
|
2376
|
+
declinedCompletionInstall: z.boolean().optional()
|
|
2377
|
+
}).strict();
|
|
2378
|
+
var userPreferencesSchema = z.object({
|
|
2379
|
+
cliExperience: cliExperienceSchema.optional()
|
|
2380
|
+
}).strict();
|
|
2381
|
+
function getPreferencesPath() {
|
|
2382
|
+
return join(getAttestItConfigDir(), "preferences.yaml");
|
|
2383
|
+
}
|
|
2384
|
+
async function loadPreferences() {
|
|
2385
|
+
const prefsPath = getPreferencesPath();
|
|
2386
|
+
try {
|
|
2387
|
+
const content = await readFile(prefsPath, "utf8");
|
|
2388
|
+
const parsed = parse(content);
|
|
2389
|
+
const result = userPreferencesSchema.safeParse(parsed);
|
|
2390
|
+
if (result.success) {
|
|
2391
|
+
const prefs = {};
|
|
2392
|
+
if (result.data.cliExperience) {
|
|
2393
|
+
prefs.cliExperience = {
|
|
2394
|
+
...result.data.cliExperience.declinedCompletionInstall !== void 0 && {
|
|
2395
|
+
declinedCompletionInstall: result.data.cliExperience.declinedCompletionInstall
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
return prefs;
|
|
2400
|
+
}
|
|
2401
|
+
console.warn("Invalid preferences file, using defaults:", result.error.message);
|
|
2402
|
+
return {};
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
2405
|
+
return {};
|
|
2406
|
+
}
|
|
2407
|
+
throw error;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
async function savePreferences(preferences) {
|
|
2411
|
+
const prefsPath = getPreferencesPath();
|
|
2412
|
+
const content = stringify(preferences);
|
|
2413
|
+
const dir = dirname(prefsPath);
|
|
2414
|
+
await mkdir(dir, { recursive: true });
|
|
2415
|
+
await writeFile(prefsPath, content, "utf8");
|
|
2416
|
+
}
|
|
2417
|
+
async function setPreference(key, value) {
|
|
2418
|
+
const prefs = await loadPreferences();
|
|
2419
|
+
prefs[key] = value;
|
|
2420
|
+
await savePreferences(prefs);
|
|
2421
|
+
}
|
|
2422
|
+
async function getPreference(key) {
|
|
2423
|
+
const prefs = await loadPreferences();
|
|
2424
|
+
return prefs[key];
|
|
2425
|
+
}
|
|
1523
2426
|
function isAuthorizedSigner(config, gateId, publicKey) {
|
|
1524
2427
|
const gate = config.gates?.[gateId];
|
|
1525
2428
|
if (!gate) {
|
|
@@ -1836,6 +2739,6 @@ function verifyAllSeals(config, seals, fingerprints) {
|
|
|
1836
2739
|
// src/index.ts
|
|
1837
2740
|
var version = "0.0.0";
|
|
1838
2741
|
|
|
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 };
|
|
2742
|
+
export { ConfigNotFoundError, ConfigValidationError, FilesystemKeyProvider, KeyProviderRegistry, LocalConfigValidationError, MacOSKeychainKeyProvider, OnePasswordKeyProvider, OperationalValidationError, PolicyValidationError, SignatureInvalidError, YubiKeyProvider, canonicalizeAttestations, computeFingerprint, computeFingerprintSync, createAttestation, createSeal, findAttestation, findConfigPath, findTeamMemberByPublicKey, generateKeyPair2 as generateEd25519KeyPair, getActiveIdentity, getAttestItConfigDir, getAttestItHomeDir, getAuthorizedSignersForGate, getGate, getLocalConfigPath, getPreference, getPreferencesPath, getPublicKeyFromPrivate, isAuthorizedSigner, listPackageFiles, loadConfig, loadConfigSync, loadLocalConfig, loadLocalConfigSync, loadPreferences, mergeConfigs, operationalSchema, parseDuration, parseOperationalContent, parsePolicyContent, policySchema, readAndVerifyAttestations, readAttestations, readAttestationsSync, readSeals, readSealsSync, removeAttestation, resolveConfigPaths, saveLocalConfig, saveLocalConfigSync, savePreferences, setAttestItHomeDir, setPreference, sign3 as signEd25519, toAttestItConfig, upsertAttestation, validateSuiteGateReferences, verifyAllSeals, verifyAttestations, verify3 as verifyEd25519, verifyGateSeal, verifySeal, version, writeAttestations, writeAttestationsSync, writeSeals, writeSealsSync, writeSignedAttestations };
|
|
1840
2743
|
//# sourceMappingURL=index.js.map
|
|
1841
2744
|
//# sourceMappingURL=index.js.map
|