@attest-it/core 0.5.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 +1198 -531
- package/dist/core-beta.d.ts +1198 -531
- package/dist/core-public.d.ts +1198 -531
- package/dist/core-unstripped.d.ts +1198 -531
- package/dist/index.cjs +980 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +743 -1
- package/dist/index.d.ts +739 -1
- package/dist/index.js +945 -104
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var child_process = require('child_process');
|
|
4
|
-
var
|
|
4
|
+
var fs8 = require('fs/promises');
|
|
5
5
|
var path2 = require('path');
|
|
6
6
|
var os = require('os');
|
|
7
7
|
var fs = require('fs');
|
|
8
8
|
var ms = require('ms');
|
|
9
9
|
var yaml = require('yaml');
|
|
10
10
|
var zod = require('zod');
|
|
11
|
-
var
|
|
11
|
+
var crypto3 = require('crypto');
|
|
12
12
|
var tinyglobby = require('tinyglobby');
|
|
13
13
|
var canonicalizeNamespace = require('canonicalize');
|
|
14
14
|
|
|
@@ -32,12 +32,12 @@ function _interopNamespace(e) {
|
|
|
32
32
|
return Object.freeze(n);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
var
|
|
35
|
+
var fs8__namespace = /*#__PURE__*/_interopNamespace(fs8);
|
|
36
36
|
var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
|
|
37
37
|
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
38
38
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
39
39
|
var ms__default = /*#__PURE__*/_interopDefault(ms);
|
|
40
|
-
var
|
|
40
|
+
var crypto3__namespace = /*#__PURE__*/_interopNamespace(crypto3);
|
|
41
41
|
var canonicalizeNamespace__namespace = /*#__PURE__*/_interopNamespace(canonicalizeNamespace);
|
|
42
42
|
|
|
43
43
|
var __defProp = Object.defineProperty;
|
|
@@ -62,7 +62,7 @@ __export(crypto_exports, {
|
|
|
62
62
|
verify: () => verify
|
|
63
63
|
});
|
|
64
64
|
async function runOpenSSL(args, stdin) {
|
|
65
|
-
return new Promise((
|
|
65
|
+
return new Promise((resolve4, reject) => {
|
|
66
66
|
const child = child_process.spawn("openssl", args, {
|
|
67
67
|
stdio: ["pipe", "pipe", "pipe"]
|
|
68
68
|
});
|
|
@@ -78,7 +78,7 @@ async function runOpenSSL(args, stdin) {
|
|
|
78
78
|
reject(new Error(`Failed to spawn OpenSSL: ${err.message}`));
|
|
79
79
|
});
|
|
80
80
|
child.on("close", (code) => {
|
|
81
|
-
|
|
81
|
+
resolve4({
|
|
82
82
|
exitCode: code ?? 1,
|
|
83
83
|
stdout: Buffer.concat(stdoutChunks),
|
|
84
84
|
stderr
|
|
@@ -120,7 +120,7 @@ function getDefaultPublicKeyPath() {
|
|
|
120
120
|
}
|
|
121
121
|
async function ensureDir(dirPath) {
|
|
122
122
|
try {
|
|
123
|
-
await
|
|
123
|
+
await fs8__namespace.mkdir(dirPath, { recursive: true });
|
|
124
124
|
} catch (err) {
|
|
125
125
|
if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
|
|
126
126
|
throw err;
|
|
@@ -129,7 +129,7 @@ async function ensureDir(dirPath) {
|
|
|
129
129
|
}
|
|
130
130
|
async function fileExists(filePath) {
|
|
131
131
|
try {
|
|
132
|
-
await
|
|
132
|
+
await fs8__namespace.access(filePath);
|
|
133
133
|
return true;
|
|
134
134
|
} catch {
|
|
135
135
|
return false;
|
|
@@ -138,7 +138,7 @@ async function fileExists(filePath) {
|
|
|
138
138
|
async function cleanupFiles(...paths) {
|
|
139
139
|
for (const filePath of paths) {
|
|
140
140
|
try {
|
|
141
|
-
await
|
|
141
|
+
await fs8__namespace.unlink(filePath);
|
|
142
142
|
} catch {
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -212,21 +212,21 @@ async function sign(options) {
|
|
|
212
212
|
}
|
|
213
213
|
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
214
214
|
const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
|
|
215
|
-
const tmpDir = await
|
|
215
|
+
const tmpDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
216
216
|
const dataFile = path2__namespace.join(tmpDir, "data.bin");
|
|
217
217
|
const sigFile = path2__namespace.join(tmpDir, "sig.bin");
|
|
218
218
|
try {
|
|
219
|
-
await
|
|
219
|
+
await fs8__namespace.writeFile(dataFile, processBuffer);
|
|
220
220
|
const signArgs = ["dgst", "-sha256", "-sign", effectiveKeyPath, "-out", sigFile, dataFile];
|
|
221
221
|
const result = await runOpenSSL(signArgs);
|
|
222
222
|
if (result.exitCode !== 0) {
|
|
223
223
|
throw new Error(`Failed to sign data: ${result.stderr}`);
|
|
224
224
|
}
|
|
225
|
-
const sigBuffer = await
|
|
225
|
+
const sigBuffer = await fs8__namespace.readFile(sigFile);
|
|
226
226
|
return sigBuffer.toString("base64");
|
|
227
227
|
} finally {
|
|
228
228
|
try {
|
|
229
|
-
await
|
|
229
|
+
await fs8__namespace.rm(tmpDir, { recursive: true, force: true });
|
|
230
230
|
} catch {
|
|
231
231
|
}
|
|
232
232
|
}
|
|
@@ -245,12 +245,12 @@ async function verify(options) {
|
|
|
245
245
|
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
246
246
|
const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
|
|
247
247
|
const sigBuffer = Buffer.from(signature, "base64");
|
|
248
|
-
const tmpDir = await
|
|
248
|
+
const tmpDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
249
249
|
const dataFile = path2__namespace.join(tmpDir, "data.bin");
|
|
250
250
|
const sigFile = path2__namespace.join(tmpDir, "sig.bin");
|
|
251
251
|
try {
|
|
252
|
-
await
|
|
253
|
-
await
|
|
252
|
+
await fs8__namespace.writeFile(dataFile, processBuffer);
|
|
253
|
+
await fs8__namespace.writeFile(sigFile, sigBuffer);
|
|
254
254
|
const verifyArgs = [
|
|
255
255
|
"dgst",
|
|
256
256
|
"-sha256",
|
|
@@ -264,16 +264,16 @@ async function verify(options) {
|
|
|
264
264
|
return result.exitCode === 0 && result.stdout.toString().includes("Verified OK");
|
|
265
265
|
} finally {
|
|
266
266
|
try {
|
|
267
|
-
await
|
|
267
|
+
await fs8__namespace.rm(tmpDir, { recursive: true, force: true });
|
|
268
268
|
} catch {
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
async function setKeyPermissions(keyPath) {
|
|
273
273
|
if (process.platform === "win32") {
|
|
274
|
-
await
|
|
274
|
+
await fs8__namespace.chmod(keyPath, 384);
|
|
275
275
|
} else {
|
|
276
|
-
await
|
|
276
|
+
await fs8__namespace.chmod(keyPath, 384);
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
var openSSLChecked;
|
|
@@ -431,7 +431,7 @@ async function loadConfig(configPath) {
|
|
|
431
431
|
);
|
|
432
432
|
}
|
|
433
433
|
try {
|
|
434
|
-
const content = await
|
|
434
|
+
const content = await fs8.readFile(resolvedPath, "utf8");
|
|
435
435
|
const format = getConfigFormat(resolvedPath);
|
|
436
436
|
return parseConfigContent(content, format);
|
|
437
437
|
} catch (error) {
|
|
@@ -521,6 +521,299 @@ function toAttestItConfig(config) {
|
|
|
521
521
|
);
|
|
522
522
|
return result;
|
|
523
523
|
}
|
|
524
|
+
var teamMemberSchema2 = zod.z.object({
|
|
525
|
+
name: zod.z.string().min(1, "Team member name cannot be empty"),
|
|
526
|
+
email: zod.z.string().email().optional(),
|
|
527
|
+
github: zod.z.string().min(1).optional(),
|
|
528
|
+
publicKey: zod.z.string().min(1, "Public key is required")
|
|
529
|
+
}).strict();
|
|
530
|
+
var fingerprintConfigSchema2 = zod.z.object({
|
|
531
|
+
paths: zod.z.array(zod.z.string().min(1, "Path cannot be empty")).min(1, "At least one path is required"),
|
|
532
|
+
exclude: zod.z.array(zod.z.string().min(1, "Exclude pattern cannot be empty")).optional()
|
|
533
|
+
}).strict();
|
|
534
|
+
var durationSchema2 = zod.z.string().refine(
|
|
535
|
+
(val) => {
|
|
536
|
+
try {
|
|
537
|
+
const parsed = ms__default.default(val);
|
|
538
|
+
return typeof parsed === "number" && parsed > 0;
|
|
539
|
+
} catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
message: 'Duration must be a valid duration string (e.g., "30d", "7d", "24h")'
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
var gateSchema2 = zod.z.object({
|
|
548
|
+
name: zod.z.string().min(1, "Gate name cannot be empty"),
|
|
549
|
+
description: zod.z.string().min(1, "Gate description cannot be empty"),
|
|
550
|
+
authorizedSigners: zod.z.array(zod.z.string().min(1, "Authorized signer slug cannot be empty")).min(1, "At least one authorized signer is required"),
|
|
551
|
+
fingerprint: fingerprintConfigSchema2,
|
|
552
|
+
maxAge: durationSchema2
|
|
553
|
+
}).strict();
|
|
554
|
+
var keyProviderOptionsSchema2 = zod.z.object({
|
|
555
|
+
privateKeyPath: zod.z.string().optional(),
|
|
556
|
+
account: zod.z.string().optional(),
|
|
557
|
+
vault: zod.z.string().optional(),
|
|
558
|
+
itemName: zod.z.string().optional()
|
|
559
|
+
}).passthrough();
|
|
560
|
+
var keyProviderSchema2 = zod.z.object({
|
|
561
|
+
type: zod.z.enum(["filesystem", "1password"]).or(zod.z.string()),
|
|
562
|
+
options: keyProviderOptionsSchema2.optional()
|
|
563
|
+
}).strict();
|
|
564
|
+
|
|
565
|
+
// src/config/policy-schema.ts
|
|
566
|
+
var policySettingsSchema = zod.z.object({
|
|
567
|
+
maxAgeDays: zod.z.number().int().positive().default(30),
|
|
568
|
+
publicKeyPath: zod.z.string().default(".attest-it/pubkey.pem"),
|
|
569
|
+
attestationsPath: zod.z.string().default(".attest-it/attestations.json")
|
|
570
|
+
}).strict();
|
|
571
|
+
var policySchema = zod.z.object({
|
|
572
|
+
version: zod.z.literal(1),
|
|
573
|
+
settings: policySettingsSchema.default({}),
|
|
574
|
+
team: zod.z.record(zod.z.string(), teamMemberSchema2).optional(),
|
|
575
|
+
gates: zod.z.record(zod.z.string(), gateSchema2).optional()
|
|
576
|
+
}).strict();
|
|
577
|
+
var PolicyValidationError = class extends Error {
|
|
578
|
+
constructor(message, issues) {
|
|
579
|
+
super(message);
|
|
580
|
+
this.issues = issues;
|
|
581
|
+
this.name = "PolicyValidationError";
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
function parsePolicyContent(content, format) {
|
|
585
|
+
let rawConfig;
|
|
586
|
+
try {
|
|
587
|
+
if (format === "yaml") {
|
|
588
|
+
rawConfig = yaml.parse(content);
|
|
589
|
+
} else {
|
|
590
|
+
rawConfig = JSON.parse(content);
|
|
591
|
+
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
throw new PolicyValidationError(
|
|
594
|
+
`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
|
|
595
|
+
[]
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const result = policySchema.safeParse(rawConfig);
|
|
599
|
+
if (!result.success) {
|
|
600
|
+
throw new PolicyValidationError(
|
|
601
|
+
"Policy validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
602
|
+
result.error.issues
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
return result.data;
|
|
606
|
+
}
|
|
607
|
+
var operationalSettingsSchema = zod.z.object({
|
|
608
|
+
defaultCommand: zod.z.string().optional(),
|
|
609
|
+
keyProvider: keyProviderSchema2.optional()
|
|
610
|
+
}).strict();
|
|
611
|
+
var suiteSchema2 = zod.z.object({
|
|
612
|
+
// Gate fields (if present, this suite references a gate)
|
|
613
|
+
gate: zod.z.string().optional(),
|
|
614
|
+
// Legacy fingerprint definition (for backward compatibility)
|
|
615
|
+
description: zod.z.string().optional(),
|
|
616
|
+
packages: zod.z.array(zod.z.string().min(1, "Package path cannot be empty")).optional(),
|
|
617
|
+
files: zod.z.array(zod.z.string().min(1, "File path cannot be empty")).optional(),
|
|
618
|
+
ignore: zod.z.array(zod.z.string().min(1, "Ignore pattern cannot be empty")).optional(),
|
|
619
|
+
// CLI-specific fields
|
|
620
|
+
command: zod.z.string().optional(),
|
|
621
|
+
timeout: zod.z.string().optional(),
|
|
622
|
+
interactive: zod.z.boolean().optional(),
|
|
623
|
+
// Relationship fields
|
|
624
|
+
invalidates: zod.z.array(zod.z.string().min(1, "Invalidated suite name cannot be empty")).optional(),
|
|
625
|
+
depends_on: zod.z.array(zod.z.string().min(1, "Dependency suite name cannot be empty")).optional()
|
|
626
|
+
}).strict().refine(
|
|
627
|
+
(suite) => {
|
|
628
|
+
return suite.gate !== void 0 || suite.packages !== void 0 && suite.packages.length > 0;
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
message: "Suite must either reference a gate or define packages for fingerprinting"
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
var operationalSchema = zod.z.object({
|
|
635
|
+
version: zod.z.literal(1),
|
|
636
|
+
settings: operationalSettingsSchema.default({}),
|
|
637
|
+
suites: zod.z.record(zod.z.string(), suiteSchema2).refine((suites) => Object.keys(suites).length >= 1, {
|
|
638
|
+
message: "At least one suite must be defined"
|
|
639
|
+
}),
|
|
640
|
+
groups: zod.z.record(zod.z.string(), zod.z.array(zod.z.string().min(1, "Suite name in group cannot be empty"))).optional()
|
|
641
|
+
}).strict();
|
|
642
|
+
var OperationalValidationError = class extends Error {
|
|
643
|
+
constructor(message, issues) {
|
|
644
|
+
super(message);
|
|
645
|
+
this.issues = issues;
|
|
646
|
+
this.name = "OperationalValidationError";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
function parseOperationalContent(content, format) {
|
|
650
|
+
let rawConfig;
|
|
651
|
+
try {
|
|
652
|
+
if (format === "yaml") {
|
|
653
|
+
rawConfig = yaml.parse(content);
|
|
654
|
+
} else {
|
|
655
|
+
rawConfig = JSON.parse(content);
|
|
656
|
+
}
|
|
657
|
+
} catch (error) {
|
|
658
|
+
throw new OperationalValidationError(
|
|
659
|
+
`Failed to parse ${format.toUpperCase()}: ${error instanceof Error ? error.message : String(error)}`,
|
|
660
|
+
[]
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
const result = operationalSchema.safeParse(rawConfig);
|
|
664
|
+
if (!result.success) {
|
|
665
|
+
throw new OperationalValidationError(
|
|
666
|
+
"Operational configuration validation failed:\n" + result.error.issues.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`).join("\n"),
|
|
667
|
+
result.error.issues
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
return result.data;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/config/merge.ts
|
|
674
|
+
function toSuiteConfig(suite) {
|
|
675
|
+
const result = {};
|
|
676
|
+
if (suite.gate !== void 0) result.gate = suite.gate;
|
|
677
|
+
if (suite.description !== void 0) result.description = suite.description;
|
|
678
|
+
if (suite.packages !== void 0) result.packages = suite.packages;
|
|
679
|
+
if (suite.files !== void 0) result.files = suite.files;
|
|
680
|
+
if (suite.ignore !== void 0) result.ignore = suite.ignore;
|
|
681
|
+
if (suite.command !== void 0) result.command = suite.command;
|
|
682
|
+
if (suite.timeout !== void 0) result.timeout = suite.timeout;
|
|
683
|
+
if (suite.interactive !== void 0) result.interactive = suite.interactive;
|
|
684
|
+
if (suite.invalidates !== void 0) result.invalidates = suite.invalidates;
|
|
685
|
+
if (suite.depends_on !== void 0) result.depends_on = suite.depends_on;
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
function toTeamMember(member) {
|
|
689
|
+
const result = {
|
|
690
|
+
name: member.name,
|
|
691
|
+
publicKey: member.publicKey
|
|
692
|
+
};
|
|
693
|
+
if (member.email !== void 0) result.email = member.email;
|
|
694
|
+
if (member.github !== void 0) result.github = member.github;
|
|
695
|
+
return result;
|
|
696
|
+
}
|
|
697
|
+
function toGateConfig(gate) {
|
|
698
|
+
const fingerprint = {
|
|
699
|
+
paths: gate.fingerprint.paths
|
|
700
|
+
};
|
|
701
|
+
if (gate.fingerprint.exclude !== void 0) {
|
|
702
|
+
fingerprint.exclude = gate.fingerprint.exclude;
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
name: gate.name,
|
|
706
|
+
description: gate.description,
|
|
707
|
+
authorizedSigners: gate.authorizedSigners,
|
|
708
|
+
fingerprint,
|
|
709
|
+
maxAge: gate.maxAge
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function toKeyProvider(provider) {
|
|
713
|
+
const result = {
|
|
714
|
+
type: provider.type
|
|
715
|
+
};
|
|
716
|
+
if (provider.options !== void 0) {
|
|
717
|
+
const options = {};
|
|
718
|
+
let hasOptions = false;
|
|
719
|
+
if (provider.options.privateKeyPath !== void 0) {
|
|
720
|
+
options.privateKeyPath = provider.options.privateKeyPath;
|
|
721
|
+
hasOptions = true;
|
|
722
|
+
}
|
|
723
|
+
if (provider.options.account !== void 0) {
|
|
724
|
+
options.account = provider.options.account;
|
|
725
|
+
hasOptions = true;
|
|
726
|
+
}
|
|
727
|
+
if (provider.options.vault !== void 0) {
|
|
728
|
+
options.vault = provider.options.vault;
|
|
729
|
+
hasOptions = true;
|
|
730
|
+
}
|
|
731
|
+
if (provider.options.itemName !== void 0) {
|
|
732
|
+
options.itemName = provider.options.itemName;
|
|
733
|
+
hasOptions = true;
|
|
734
|
+
}
|
|
735
|
+
if (hasOptions) {
|
|
736
|
+
result.options = options;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
function mergeConfigs(policy, operational) {
|
|
742
|
+
const settings = {
|
|
743
|
+
// Security settings from policy (these are trust-critical)
|
|
744
|
+
maxAgeDays: policy.settings.maxAgeDays,
|
|
745
|
+
publicKeyPath: policy.settings.publicKeyPath,
|
|
746
|
+
attestationsPath: policy.settings.attestationsPath
|
|
747
|
+
};
|
|
748
|
+
if (operational.settings.defaultCommand !== void 0) {
|
|
749
|
+
settings.defaultCommand = operational.settings.defaultCommand;
|
|
750
|
+
}
|
|
751
|
+
if (operational.settings.keyProvider !== void 0) {
|
|
752
|
+
settings.keyProvider = toKeyProvider(operational.settings.keyProvider);
|
|
753
|
+
}
|
|
754
|
+
const suites = {};
|
|
755
|
+
for (const [name, suite] of Object.entries(operational.suites)) {
|
|
756
|
+
suites[name] = toSuiteConfig(suite);
|
|
757
|
+
}
|
|
758
|
+
const config = {
|
|
759
|
+
version: 1,
|
|
760
|
+
settings,
|
|
761
|
+
suites
|
|
762
|
+
};
|
|
763
|
+
if (policy.team !== void 0) {
|
|
764
|
+
const team = {};
|
|
765
|
+
for (const [slug, member] of Object.entries(policy.team)) {
|
|
766
|
+
team[slug] = toTeamMember(member);
|
|
767
|
+
}
|
|
768
|
+
config.team = team;
|
|
769
|
+
}
|
|
770
|
+
if (policy.gates !== void 0) {
|
|
771
|
+
const gates = {};
|
|
772
|
+
for (const [slug, gate] of Object.entries(policy.gates)) {
|
|
773
|
+
gates[slug] = toGateConfig(gate);
|
|
774
|
+
}
|
|
775
|
+
config.gates = gates;
|
|
776
|
+
}
|
|
777
|
+
if (operational.groups !== void 0) {
|
|
778
|
+
config.groups = operational.groups;
|
|
779
|
+
}
|
|
780
|
+
return config;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/config/validation.ts
|
|
784
|
+
function validateSuiteGateReferences(policy, operational) {
|
|
785
|
+
const errors = [];
|
|
786
|
+
const gates = policy.gates ?? {};
|
|
787
|
+
const team = policy.team ?? {};
|
|
788
|
+
for (const [suiteName, suiteConfig] of Object.entries(operational.suites)) {
|
|
789
|
+
const gateName = suiteConfig.gate;
|
|
790
|
+
if (gateName === void 0) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
const gate = gates[gateName];
|
|
794
|
+
if (gate === void 0) {
|
|
795
|
+
errors.push({
|
|
796
|
+
type: "UNKNOWN_GATE",
|
|
797
|
+
suite: suiteName,
|
|
798
|
+
gate: gateName,
|
|
799
|
+
message: `Suite "${suiteName}" references unknown gate "${gateName}". The gate must be defined in policy.yaml.`
|
|
800
|
+
});
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
for (const signerSlug of gate.authorizedSigners) {
|
|
804
|
+
if (team[signerSlug] === void 0) {
|
|
805
|
+
errors.push({
|
|
806
|
+
type: "MISSING_TEAM_MEMBER",
|
|
807
|
+
suite: suiteName,
|
|
808
|
+
gate: gateName,
|
|
809
|
+
signer: signerSlug,
|
|
810
|
+
message: `Gate "${gateName}" (referenced by suite "${suiteName}") authorizes signer "${signerSlug}", but this team member is not defined in policy.yaml.`
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return errors;
|
|
816
|
+
}
|
|
524
817
|
var LARGE_FILE_THRESHOLD = 50 * 1024 * 1024;
|
|
525
818
|
function sortFiles(files) {
|
|
526
819
|
return [...files].sort((a, b) => {
|
|
@@ -540,13 +833,13 @@ function computeFinalFingerprint(fileHashes) {
|
|
|
540
833
|
});
|
|
541
834
|
const hashes = sorted.map((input) => input.hash);
|
|
542
835
|
const concatenated = Buffer.concat(hashes);
|
|
543
|
-
const finalHash =
|
|
836
|
+
const finalHash = crypto3__namespace.createHash("sha256").update(concatenated).digest();
|
|
544
837
|
return `sha256:${finalHash.toString("hex")}`;
|
|
545
838
|
}
|
|
546
839
|
async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
547
840
|
if (stats.size > LARGE_FILE_THRESHOLD) {
|
|
548
|
-
return new Promise((
|
|
549
|
-
const hash2 =
|
|
841
|
+
return new Promise((resolve4, reject) => {
|
|
842
|
+
const hash2 = crypto3__namespace.createHash("sha256");
|
|
550
843
|
hash2.update(normalizedPath);
|
|
551
844
|
hash2.update(":");
|
|
552
845
|
const stream = fs__namespace.createReadStream(realPath);
|
|
@@ -554,13 +847,13 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
554
847
|
hash2.update(chunk);
|
|
555
848
|
});
|
|
556
849
|
stream.on("end", () => {
|
|
557
|
-
|
|
850
|
+
resolve4(hash2.digest());
|
|
558
851
|
});
|
|
559
852
|
stream.on("error", reject);
|
|
560
853
|
});
|
|
561
854
|
}
|
|
562
855
|
const content = await fs__namespace.promises.readFile(realPath);
|
|
563
|
-
const hash =
|
|
856
|
+
const hash = crypto3__namespace.createHash("sha256");
|
|
564
857
|
hash.update(normalizedPath);
|
|
565
858
|
hash.update(":");
|
|
566
859
|
hash.update(content);
|
|
@@ -568,7 +861,7 @@ async function hashFileAsync(realPath, normalizedPath, stats) {
|
|
|
568
861
|
}
|
|
569
862
|
function hashFileSync(realPath, normalizedPath) {
|
|
570
863
|
const content = fs__namespace.readFileSync(realPath);
|
|
571
|
-
const hash =
|
|
864
|
+
const hash = crypto3__namespace.createHash("sha256");
|
|
572
865
|
hash.update(normalizedPath);
|
|
573
866
|
hash.update(":");
|
|
574
867
|
hash.update(content);
|
|
@@ -876,7 +1169,7 @@ function isBuffer(value) {
|
|
|
876
1169
|
}
|
|
877
1170
|
function generateKeyPair2() {
|
|
878
1171
|
try {
|
|
879
|
-
const keyPair =
|
|
1172
|
+
const keyPair = crypto3__namespace.generateKeyPairSync("ed25519", {
|
|
880
1173
|
publicKeyEncoding: {
|
|
881
1174
|
type: "spki",
|
|
882
1175
|
format: "pem"
|
|
@@ -890,7 +1183,7 @@ function generateKeyPair2() {
|
|
|
890
1183
|
if (typeof publicKey !== "string" || typeof privateKey !== "string") {
|
|
891
1184
|
throw new Error("Expected keypair to have string keys");
|
|
892
1185
|
}
|
|
893
|
-
const publicKeyObj =
|
|
1186
|
+
const publicKeyObj = crypto3__namespace.createPublicKey(publicKey);
|
|
894
1187
|
const publicKeyExport = publicKeyObj.export({
|
|
895
1188
|
type: "spki",
|
|
896
1189
|
format: "der"
|
|
@@ -913,8 +1206,8 @@ function generateKeyPair2() {
|
|
|
913
1206
|
function sign3(data, privateKeyPem) {
|
|
914
1207
|
try {
|
|
915
1208
|
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
916
|
-
const privateKeyObj =
|
|
917
|
-
const signatureResult =
|
|
1209
|
+
const privateKeyObj = crypto3__namespace.createPrivateKey(privateKeyPem);
|
|
1210
|
+
const signatureResult = crypto3__namespace.sign(null, dataBuffer, privateKeyObj);
|
|
918
1211
|
if (!isBuffer(signatureResult)) {
|
|
919
1212
|
throw new Error("Expected signature to be a Buffer");
|
|
920
1213
|
}
|
|
@@ -954,12 +1247,12 @@ function verify3(data, signature, publicKeyBase64) {
|
|
|
954
1247
|
// BIT STRING, 33 bytes (32 key + 1 padding)
|
|
955
1248
|
]);
|
|
956
1249
|
const spkiBuffer = Buffer.concat([spkiHeader, rawPublicKey]);
|
|
957
|
-
const publicKeyObj =
|
|
1250
|
+
const publicKeyObj = crypto3__namespace.createPublicKey({
|
|
958
1251
|
key: spkiBuffer,
|
|
959
1252
|
format: "der",
|
|
960
1253
|
type: "spki"
|
|
961
1254
|
});
|
|
962
|
-
return
|
|
1255
|
+
return crypto3__namespace.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
|
|
963
1256
|
} catch (err) {
|
|
964
1257
|
if (err instanceof Error && err.message.includes("verification failed")) {
|
|
965
1258
|
return false;
|
|
@@ -971,8 +1264,8 @@ function verify3(data, signature, publicKeyBase64) {
|
|
|
971
1264
|
}
|
|
972
1265
|
function getPublicKeyFromPrivate(privateKeyPem) {
|
|
973
1266
|
try {
|
|
974
|
-
const privateKeyObj =
|
|
975
|
-
const publicKeyObj =
|
|
1267
|
+
const privateKeyObj = crypto3__namespace.createPrivateKey(privateKeyPem);
|
|
1268
|
+
const publicKeyObj = crypto3__namespace.createPublicKey(privateKeyObj);
|
|
976
1269
|
const publicKeyExport = publicKeyObj.export({
|
|
977
1270
|
type: "spki",
|
|
978
1271
|
format: "der"
|
|
@@ -1141,7 +1434,7 @@ var FilesystemKeyProvider = class {
|
|
|
1141
1434
|
*/
|
|
1142
1435
|
async keyExists(keyRef) {
|
|
1143
1436
|
try {
|
|
1144
|
-
await
|
|
1437
|
+
await fs8__namespace.access(keyRef);
|
|
1145
1438
|
return true;
|
|
1146
1439
|
} catch {
|
|
1147
1440
|
return false;
|
|
@@ -1302,7 +1595,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1302
1595
|
`Key not found in 1Password: "${keyRef}" (vault: ${this.vault})` + (this.account ? ` (account: ${this.account})` : "")
|
|
1303
1596
|
);
|
|
1304
1597
|
}
|
|
1305
|
-
const tempDir = await
|
|
1598
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
1306
1599
|
const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1307
1600
|
try {
|
|
1308
1601
|
const args = ["document", "get", keyRef, "--vault", this.vault, "--out-file", tempKeyPath];
|
|
@@ -1315,8 +1608,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1315
1608
|
keyPath: tempKeyPath,
|
|
1316
1609
|
cleanup: async () => {
|
|
1317
1610
|
try {
|
|
1318
|
-
await
|
|
1319
|
-
await
|
|
1611
|
+
await fs8__namespace.unlink(tempKeyPath);
|
|
1612
|
+
await fs8__namespace.rmdir(tempDir);
|
|
1320
1613
|
} catch (cleanupError) {
|
|
1321
1614
|
console.warn(
|
|
1322
1615
|
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1326,7 +1619,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1326
1619
|
};
|
|
1327
1620
|
} catch (error) {
|
|
1328
1621
|
try {
|
|
1329
|
-
await
|
|
1622
|
+
await fs8__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1330
1623
|
} catch (cleanupError) {
|
|
1331
1624
|
console.warn(
|
|
1332
1625
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1342,7 +1635,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1342
1635
|
*/
|
|
1343
1636
|
async generateKeyPair(options) {
|
|
1344
1637
|
const { publicKeyPath, force = false } = options;
|
|
1345
|
-
const tempDir = await
|
|
1638
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
|
|
1346
1639
|
const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1347
1640
|
try {
|
|
1348
1641
|
await generateKeyPair({
|
|
@@ -1363,8 +1656,8 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1363
1656
|
args.push("--account", this.account);
|
|
1364
1657
|
}
|
|
1365
1658
|
await execCommand("op", args);
|
|
1366
|
-
await
|
|
1367
|
-
await
|
|
1659
|
+
await fs8__namespace.unlink(tempPrivateKeyPath);
|
|
1660
|
+
await fs8__namespace.rmdir(tempDir);
|
|
1368
1661
|
return {
|
|
1369
1662
|
privateKeyRef: this.itemName,
|
|
1370
1663
|
publicKeyPath,
|
|
@@ -1372,7 +1665,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1372
1665
|
};
|
|
1373
1666
|
} catch (error) {
|
|
1374
1667
|
try {
|
|
1375
|
-
await
|
|
1668
|
+
await fs8__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1376
1669
|
} catch (cleanupError) {
|
|
1377
1670
|
console.warn(
|
|
1378
1671
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1396,7 +1689,7 @@ var OnePasswordKeyProvider = class _OnePasswordKeyProvider {
|
|
|
1396
1689
|
}
|
|
1397
1690
|
};
|
|
1398
1691
|
async function execCommand(command, args) {
|
|
1399
|
-
return new Promise((
|
|
1692
|
+
return new Promise((resolve4, reject) => {
|
|
1400
1693
|
const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1401
1694
|
let stdout = "";
|
|
1402
1695
|
let stderr = "";
|
|
@@ -1408,7 +1701,7 @@ async function execCommand(command, args) {
|
|
|
1408
1701
|
});
|
|
1409
1702
|
proc.on("close", (code) => {
|
|
1410
1703
|
if (code === 0) {
|
|
1411
|
-
|
|
1704
|
+
resolve4(stdout.trim());
|
|
1412
1705
|
} else {
|
|
1413
1706
|
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1414
1707
|
}
|
|
@@ -1504,7 +1797,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1504
1797
|
`Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
|
|
1505
1798
|
);
|
|
1506
1799
|
}
|
|
1507
|
-
const tempDir = await
|
|
1800
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
1508
1801
|
const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1509
1802
|
try {
|
|
1510
1803
|
const findArgs = [
|
|
@@ -1520,14 +1813,14 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1520
1813
|
}
|
|
1521
1814
|
const base64Key = await execCommand2("security", findArgs);
|
|
1522
1815
|
const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
|
|
1523
|
-
await
|
|
1816
|
+
await fs8__namespace.writeFile(tempKeyPath, keyContent, { mode: 384 });
|
|
1524
1817
|
await setKeyPermissions(tempKeyPath);
|
|
1525
1818
|
return {
|
|
1526
1819
|
keyPath: tempKeyPath,
|
|
1527
1820
|
cleanup: async () => {
|
|
1528
1821
|
try {
|
|
1529
|
-
await
|
|
1530
|
-
await
|
|
1822
|
+
await fs8__namespace.unlink(tempKeyPath);
|
|
1823
|
+
await fs8__namespace.rmdir(tempDir);
|
|
1531
1824
|
} catch (cleanupError) {
|
|
1532
1825
|
console.warn(
|
|
1533
1826
|
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1537,7 +1830,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1537
1830
|
};
|
|
1538
1831
|
} catch (error) {
|
|
1539
1832
|
try {
|
|
1540
|
-
await
|
|
1833
|
+
await fs8__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1541
1834
|
} catch (cleanupError) {
|
|
1542
1835
|
console.warn(
|
|
1543
1836
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1553,7 +1846,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1553
1846
|
*/
|
|
1554
1847
|
async generateKeyPair(options) {
|
|
1555
1848
|
const { publicKeyPath, force = false } = options;
|
|
1556
|
-
const tempDir = await
|
|
1849
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
|
|
1557
1850
|
const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1558
1851
|
try {
|
|
1559
1852
|
await generateKeyPair({
|
|
@@ -1561,7 +1854,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1561
1854
|
publicPath: publicKeyPath,
|
|
1562
1855
|
force
|
|
1563
1856
|
});
|
|
1564
|
-
const privateKeyContent = await
|
|
1857
|
+
const privateKeyContent = await fs8__namespace.readFile(tempPrivateKeyPath, "utf8");
|
|
1565
1858
|
const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
|
|
1566
1859
|
const addArgs = [
|
|
1567
1860
|
"add-generic-password",
|
|
@@ -1579,8 +1872,8 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1579
1872
|
addArgs.push(this.keychain);
|
|
1580
1873
|
}
|
|
1581
1874
|
await execCommand2("security", addArgs);
|
|
1582
|
-
await
|
|
1583
|
-
await
|
|
1875
|
+
await fs8__namespace.unlink(tempPrivateKeyPath);
|
|
1876
|
+
await fs8__namespace.rmdir(tempDir);
|
|
1584
1877
|
return {
|
|
1585
1878
|
privateKeyRef: this.itemName,
|
|
1586
1879
|
publicKeyPath,
|
|
@@ -1588,7 +1881,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1588
1881
|
};
|
|
1589
1882
|
} catch (error) {
|
|
1590
1883
|
try {
|
|
1591
|
-
await
|
|
1884
|
+
await fs8__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1592
1885
|
} catch (cleanupError) {
|
|
1593
1886
|
console.warn(
|
|
1594
1887
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1610,7 +1903,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1610
1903
|
}
|
|
1611
1904
|
};
|
|
1612
1905
|
async function execCommand2(command, args) {
|
|
1613
|
-
return new Promise((
|
|
1906
|
+
return new Promise((resolve4, reject) => {
|
|
1614
1907
|
const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1615
1908
|
let stdout = "";
|
|
1616
1909
|
let stderr = "";
|
|
@@ -1622,7 +1915,7 @@ async function execCommand2(command, args) {
|
|
|
1622
1915
|
});
|
|
1623
1916
|
proc.on("close", (code) => {
|
|
1624
1917
|
if (code === 0) {
|
|
1625
|
-
|
|
1918
|
+
resolve4(stdout.trim());
|
|
1626
1919
|
} else {
|
|
1627
1920
|
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1628
1921
|
}
|
|
@@ -1633,68 +1926,8 @@ async function execCommand2(command, args) {
|
|
|
1633
1926
|
});
|
|
1634
1927
|
}
|
|
1635
1928
|
|
|
1636
|
-
// src/key-provider/
|
|
1637
|
-
|
|
1638
|
-
static providers = /* @__PURE__ */ new Map();
|
|
1639
|
-
/**
|
|
1640
|
-
* Register a key provider factory.
|
|
1641
|
-
* @param type - Provider type identifier
|
|
1642
|
-
* @param factory - Factory function to create provider instances
|
|
1643
|
-
*/
|
|
1644
|
-
static register(type, factory) {
|
|
1645
|
-
this.providers.set(type, factory);
|
|
1646
|
-
}
|
|
1647
|
-
/**
|
|
1648
|
-
* Create a key provider from configuration.
|
|
1649
|
-
* @param config - Provider configuration
|
|
1650
|
-
* @returns A key provider instance
|
|
1651
|
-
* @throws Error if the provider type is not registered
|
|
1652
|
-
*/
|
|
1653
|
-
static create(config) {
|
|
1654
|
-
const factory = this.providers.get(config.type);
|
|
1655
|
-
if (!factory) {
|
|
1656
|
-
throw new Error(
|
|
1657
|
-
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
1658
|
-
);
|
|
1659
|
-
}
|
|
1660
|
-
return factory(config);
|
|
1661
|
-
}
|
|
1662
|
-
/**
|
|
1663
|
-
* Get all registered provider types.
|
|
1664
|
-
* @returns Array of provider type identifiers
|
|
1665
|
-
*/
|
|
1666
|
-
static getProviderTypes() {
|
|
1667
|
-
return Array.from(this.providers.keys());
|
|
1668
|
-
}
|
|
1669
|
-
};
|
|
1670
|
-
KeyProviderRegistry.register("filesystem", (config) => {
|
|
1671
|
-
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
1672
|
-
if (privateKeyPath !== void 0) {
|
|
1673
|
-
return new FilesystemKeyProvider({ privateKeyPath });
|
|
1674
|
-
}
|
|
1675
|
-
return new FilesystemKeyProvider();
|
|
1676
|
-
});
|
|
1677
|
-
KeyProviderRegistry.register("1password", (config) => {
|
|
1678
|
-
const { options } = config;
|
|
1679
|
-
const account = typeof options.account === "string" ? options.account : void 0;
|
|
1680
|
-
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
1681
|
-
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1682
|
-
if (!vault || !itemName) {
|
|
1683
|
-
throw new Error("1Password provider requires vault and itemName options");
|
|
1684
|
-
}
|
|
1685
|
-
if (account !== void 0) {
|
|
1686
|
-
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
1687
|
-
}
|
|
1688
|
-
return new OnePasswordKeyProvider({ vault, itemName });
|
|
1689
|
-
});
|
|
1690
|
-
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
1691
|
-
const { options } = config;
|
|
1692
|
-
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1693
|
-
if (!itemName) {
|
|
1694
|
-
throw new Error("macOS Keychain provider requires itemName option");
|
|
1695
|
-
}
|
|
1696
|
-
return new MacOSKeychainKeyProvider({ itemName });
|
|
1697
|
-
});
|
|
1929
|
+
// src/key-provider/yubikey-provider.ts
|
|
1930
|
+
init_crypto();
|
|
1698
1931
|
var homeDirOverride = null;
|
|
1699
1932
|
function setAttestItHomeDir(dir) {
|
|
1700
1933
|
homeDirOverride = dir;
|
|
@@ -1816,7 +2049,7 @@ function parseLocalConfigContent(content) {
|
|
|
1816
2049
|
async function loadLocalConfig(configPath) {
|
|
1817
2050
|
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1818
2051
|
try {
|
|
1819
|
-
const content = await
|
|
2052
|
+
const content = await fs8.readFile(resolvedPath, "utf8");
|
|
1820
2053
|
return parseLocalConfigContent(content);
|
|
1821
2054
|
} catch (error) {
|
|
1822
2055
|
if (error instanceof LocalConfigValidationError) {
|
|
@@ -1847,8 +2080,8 @@ async function saveLocalConfig(config, configPath) {
|
|
|
1847
2080
|
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1848
2081
|
const content = yaml.stringify(config);
|
|
1849
2082
|
const dir = path2.dirname(resolvedPath);
|
|
1850
|
-
await
|
|
1851
|
-
await
|
|
2083
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
2084
|
+
await fs8.writeFile(resolvedPath, content, "utf8");
|
|
1852
2085
|
}
|
|
1853
2086
|
function saveLocalConfigSync(config, configPath) {
|
|
1854
2087
|
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
@@ -1860,6 +2093,617 @@ function saveLocalConfigSync(config, configPath) {
|
|
|
1860
2093
|
function getActiveIdentity(config) {
|
|
1861
2094
|
return config.identities[config.activeIdentity];
|
|
1862
2095
|
}
|
|
2096
|
+
|
|
2097
|
+
// src/key-provider/yubikey-provider.ts
|
|
2098
|
+
var EncryptedKeyFileSchema = zod.z.object({
|
|
2099
|
+
version: zod.z.literal(1),
|
|
2100
|
+
iv: zod.z.string().min(1),
|
|
2101
|
+
authTag: zod.z.string().min(1),
|
|
2102
|
+
salt: zod.z.string().min(1),
|
|
2103
|
+
challenge: zod.z.string().min(1),
|
|
2104
|
+
ciphertext: zod.z.string().min(1),
|
|
2105
|
+
slot: zod.z.union([zod.z.literal(1), zod.z.literal(2)]),
|
|
2106
|
+
serial: zod.z.string().optional(),
|
|
2107
|
+
aad: zod.z.string().optional()
|
|
2108
|
+
});
|
|
2109
|
+
var activeCleanupHandlers = /* @__PURE__ */ new Set();
|
|
2110
|
+
var processHandlersInstalled = false;
|
|
2111
|
+
function installProcessHandlers() {
|
|
2112
|
+
if (processHandlersInstalled) return;
|
|
2113
|
+
processHandlersInstalled = true;
|
|
2114
|
+
const runCleanup = async () => {
|
|
2115
|
+
const handlers = Array.from(activeCleanupHandlers);
|
|
2116
|
+
await Promise.allSettled(handlers.map((h) => h()));
|
|
2117
|
+
};
|
|
2118
|
+
process.once("beforeExit", () => {
|
|
2119
|
+
void runCleanup();
|
|
2120
|
+
});
|
|
2121
|
+
process.once("SIGINT", () => {
|
|
2122
|
+
void runCleanup().finally(() => process.exit(130));
|
|
2123
|
+
});
|
|
2124
|
+
process.once("SIGTERM", () => {
|
|
2125
|
+
void runCleanup().finally(() => process.exit(143));
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
function validateEncryptedKeyFile(data) {
|
|
2129
|
+
const parsed = EncryptedKeyFileSchema.parse(data);
|
|
2130
|
+
const iv = Buffer.from(parsed.iv, "base64");
|
|
2131
|
+
if (iv.length !== 12) {
|
|
2132
|
+
throw new Error(`Invalid IV size: expected 12 bytes, got ${String(iv.length)}`);
|
|
2133
|
+
}
|
|
2134
|
+
const authTag = Buffer.from(parsed.authTag, "base64");
|
|
2135
|
+
if (authTag.length !== 16) {
|
|
2136
|
+
throw new Error(`Invalid auth tag size: expected 16 bytes, got ${String(authTag.length)}`);
|
|
2137
|
+
}
|
|
2138
|
+
const salt = Buffer.from(parsed.salt, "base64");
|
|
2139
|
+
if (salt.length !== 32) {
|
|
2140
|
+
throw new Error(`Invalid salt size: expected 32 bytes, got ${String(salt.length)}`);
|
|
2141
|
+
}
|
|
2142
|
+
const challenge = Buffer.from(parsed.challenge, "base64");
|
|
2143
|
+
if (challenge.length !== 32) {
|
|
2144
|
+
throw new Error(`Invalid challenge size: expected 32 bytes, got ${String(challenge.length)}`);
|
|
2145
|
+
}
|
|
2146
|
+
return parsed;
|
|
2147
|
+
}
|
|
2148
|
+
function constructAAD(version2, slot, serial) {
|
|
2149
|
+
const aadObject = {
|
|
2150
|
+
version: version2,
|
|
2151
|
+
slot,
|
|
2152
|
+
serial: serial ?? "unspecified"
|
|
2153
|
+
};
|
|
2154
|
+
return Buffer.from(JSON.stringify(aadObject), "utf8");
|
|
2155
|
+
}
|
|
2156
|
+
var YubiKeyProvider = class _YubiKeyProvider {
|
|
2157
|
+
type = "yubikey";
|
|
2158
|
+
displayName = "YubiKey";
|
|
2159
|
+
encryptedKeyPath;
|
|
2160
|
+
slot;
|
|
2161
|
+
serial;
|
|
2162
|
+
/**
|
|
2163
|
+
* Create a new YubiKeyProvider.
|
|
2164
|
+
* @param options - Provider options
|
|
2165
|
+
* @throws Error if encryptedKeyPath is outside the attest-it config directory
|
|
2166
|
+
*/
|
|
2167
|
+
constructor(options) {
|
|
2168
|
+
const resolvedPath = path2__namespace.resolve(options.encryptedKeyPath);
|
|
2169
|
+
const configDir = getAttestItConfigDir();
|
|
2170
|
+
if (!resolvedPath.startsWith(configDir)) {
|
|
2171
|
+
throw new Error(
|
|
2172
|
+
`Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
this.encryptedKeyPath = resolvedPath;
|
|
2176
|
+
this.slot = options.slot ?? 2;
|
|
2177
|
+
if (options.serial !== void 0) {
|
|
2178
|
+
this.serial = options.serial;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Check if ykman CLI is installed and available.
|
|
2183
|
+
* @returns true if ykman is available
|
|
2184
|
+
*/
|
|
2185
|
+
static async isInstalled() {
|
|
2186
|
+
try {
|
|
2187
|
+
await execCommand3("ykman", ["--version"]);
|
|
2188
|
+
return true;
|
|
2189
|
+
} catch {
|
|
2190
|
+
return false;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Check if any YubiKey is connected.
|
|
2195
|
+
* @returns true if at least one YubiKey is connected
|
|
2196
|
+
*/
|
|
2197
|
+
static async isConnected() {
|
|
2198
|
+
try {
|
|
2199
|
+
const output = await execCommand3("ykman", ["list", "--serials"]);
|
|
2200
|
+
return output.trim().length > 0;
|
|
2201
|
+
} catch {
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* Check if HMAC challenge-response is configured on a slot.
|
|
2207
|
+
* @param slot - Slot number (1 or 2)
|
|
2208
|
+
* @param serial - Optional YubiKey serial number
|
|
2209
|
+
* @returns true if challenge-response is configured
|
|
2210
|
+
*/
|
|
2211
|
+
static async isChallengeResponseConfigured(slot = 2, serial) {
|
|
2212
|
+
try {
|
|
2213
|
+
const args = ["otp", "info"];
|
|
2214
|
+
if (serial) {
|
|
2215
|
+
args.unshift("--device", serial);
|
|
2216
|
+
}
|
|
2217
|
+
const output = await execCommand3("ykman", args);
|
|
2218
|
+
const slotPattern = new RegExp(`Slot ${String(slot)}:\\s+programmed.*challenge-response`, "i");
|
|
2219
|
+
return slotPattern.test(output);
|
|
2220
|
+
} catch {
|
|
2221
|
+
return false;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
/**
|
|
2225
|
+
* List connected YubiKeys.
|
|
2226
|
+
* @returns Array of YubiKey information
|
|
2227
|
+
*/
|
|
2228
|
+
static async listDevices() {
|
|
2229
|
+
if (!await _YubiKeyProvider.isInstalled()) {
|
|
2230
|
+
return [];
|
|
2231
|
+
}
|
|
2232
|
+
try {
|
|
2233
|
+
const output = await execCommand3("ykman", ["list", "--serials"]);
|
|
2234
|
+
const serials = output.trim().split("\n").filter((s) => s.length > 0);
|
|
2235
|
+
const devices = [];
|
|
2236
|
+
for (const serial of serials) {
|
|
2237
|
+
try {
|
|
2238
|
+
const infoOutput = await execCommand3("ykman", ["--device", serial, "info"]);
|
|
2239
|
+
const typeMatch = /Device type:\s+(.+)/i.exec(infoOutput);
|
|
2240
|
+
const fwMatch = /Firmware version:\s+(.+)/i.exec(infoOutput);
|
|
2241
|
+
devices.push({
|
|
2242
|
+
serial,
|
|
2243
|
+
type: typeMatch?.[1]?.trim() ?? "YubiKey",
|
|
2244
|
+
firmware: fwMatch?.[1]?.trim() ?? "Unknown"
|
|
2245
|
+
});
|
|
2246
|
+
} catch {
|
|
2247
|
+
devices.push({
|
|
2248
|
+
serial,
|
|
2249
|
+
type: "YubiKey",
|
|
2250
|
+
firmware: "Unknown"
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
return devices;
|
|
2255
|
+
} catch {
|
|
2256
|
+
return [];
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Check if this provider is available on the current system.
|
|
2261
|
+
* Requires ykman to be installed.
|
|
2262
|
+
*/
|
|
2263
|
+
async isAvailable() {
|
|
2264
|
+
return _YubiKeyProvider.isInstalled();
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Check if an encrypted key file exists.
|
|
2268
|
+
* @param keyRef - Path to encrypted key file
|
|
2269
|
+
*/
|
|
2270
|
+
async keyExists(keyRef) {
|
|
2271
|
+
try {
|
|
2272
|
+
await fs8__namespace.access(keyRef);
|
|
2273
|
+
return true;
|
|
2274
|
+
} catch {
|
|
2275
|
+
return false;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Get the private key by decrypting with YubiKey.
|
|
2280
|
+
* Downloads to a temporary file and returns a cleanup function.
|
|
2281
|
+
*
|
|
2282
|
+
* **Important**: Always call the cleanup function when done to securely delete
|
|
2283
|
+
* the temporary key file. The cleanup is also registered for process exit handlers.
|
|
2284
|
+
*
|
|
2285
|
+
* @param keyRef - Path to encrypted key file
|
|
2286
|
+
* @throws Error if the key cannot be decrypted
|
|
2287
|
+
*/
|
|
2288
|
+
async getPrivateKey(keyRef) {
|
|
2289
|
+
installProcessHandlers();
|
|
2290
|
+
if (!await this.keyExists(keyRef)) {
|
|
2291
|
+
throw new Error(`Encrypted key file not found: ${keyRef}`);
|
|
2292
|
+
}
|
|
2293
|
+
const encryptedData = await fs8__namespace.readFile(keyRef, "utf8");
|
|
2294
|
+
let keyFile;
|
|
2295
|
+
try {
|
|
2296
|
+
const parsed = JSON.parse(encryptedData);
|
|
2297
|
+
keyFile = validateEncryptedKeyFile(parsed);
|
|
2298
|
+
} catch (err) {
|
|
2299
|
+
if (err instanceof zod.z.ZodError) {
|
|
2300
|
+
throw new Error(
|
|
2301
|
+
`Invalid encrypted key file format: ${err.errors.map((e) => e.message).join(", ")}`
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
throw new Error(`Invalid encrypted key file: malformed JSON or structure`);
|
|
2305
|
+
}
|
|
2306
|
+
const expectedSerial = this.serial ?? keyFile.serial;
|
|
2307
|
+
if (!expectedSerial) {
|
|
2308
|
+
console.warn(
|
|
2309
|
+
"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."
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
if (expectedSerial) {
|
|
2313
|
+
const devices = await _YubiKeyProvider.listDevices();
|
|
2314
|
+
const matchingDevice = devices.find((d) => d.serial === expectedSerial);
|
|
2315
|
+
if (!matchingDevice) {
|
|
2316
|
+
throw new Error(
|
|
2317
|
+
`Required YubiKey not found. Expected serial: ${expectedSerial}. Connected devices: ${devices.map((d) => d.serial).join(", ") || "none"}`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
const challenge = Buffer.from(keyFile.challenge, "base64");
|
|
2322
|
+
const response = await performChallengeResponse(challenge, keyFile.slot, expectedSerial);
|
|
2323
|
+
const salt = Buffer.from(keyFile.salt, "base64");
|
|
2324
|
+
const aesKey = deriveKey(response, salt);
|
|
2325
|
+
const iv = Buffer.from(keyFile.iv, "base64");
|
|
2326
|
+
const authTag = Buffer.from(keyFile.authTag, "base64");
|
|
2327
|
+
const ciphertext = Buffer.from(keyFile.ciphertext, "base64");
|
|
2328
|
+
let privateKeyContent;
|
|
2329
|
+
try {
|
|
2330
|
+
const decipher = crypto3__namespace.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
2331
|
+
if (keyFile.aad) {
|
|
2332
|
+
decipher.setAAD(Buffer.from(keyFile.aad, "base64"));
|
|
2333
|
+
}
|
|
2334
|
+
decipher.setAuthTag(authTag);
|
|
2335
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
2336
|
+
privateKeyContent = decrypted.toString("utf8");
|
|
2337
|
+
} catch {
|
|
2338
|
+
throw new Error(
|
|
2339
|
+
"Failed to decrypt private key. Verify you are using the correct YubiKey and the encrypted key file has not been corrupted or tampered with."
|
|
2340
|
+
);
|
|
2341
|
+
}
|
|
2342
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
2343
|
+
const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
2344
|
+
const cleanup = async () => {
|
|
2345
|
+
activeCleanupHandlers.delete(cleanup);
|
|
2346
|
+
try {
|
|
2347
|
+
const keySize = Buffer.byteLength(privateKeyContent);
|
|
2348
|
+
await fs8__namespace.writeFile(tempKeyPath, crypto3__namespace.randomBytes(keySize));
|
|
2349
|
+
await fs8__namespace.unlink(tempKeyPath);
|
|
2350
|
+
await fs8__namespace.rmdir(tempDir);
|
|
2351
|
+
} catch {
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
try {
|
|
2355
|
+
await fs8__namespace.writeFile(tempKeyPath, privateKeyContent, { mode: 384 });
|
|
2356
|
+
await setKeyPermissions(tempKeyPath);
|
|
2357
|
+
activeCleanupHandlers.add(cleanup);
|
|
2358
|
+
return {
|
|
2359
|
+
keyPath: tempKeyPath,
|
|
2360
|
+
cleanup
|
|
2361
|
+
};
|
|
2362
|
+
} catch (error) {
|
|
2363
|
+
await cleanup();
|
|
2364
|
+
throw error;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Generate a new keypair and store encrypted with YubiKey.
|
|
2369
|
+
* Public key is written to filesystem for repository commit.
|
|
2370
|
+
*
|
|
2371
|
+
* **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
|
|
2372
|
+
*
|
|
2373
|
+
* @param options - Key generation options
|
|
2374
|
+
*/
|
|
2375
|
+
async generateKeyPair(options) {
|
|
2376
|
+
const { publicKeyPath, force = false } = options;
|
|
2377
|
+
if (!await _YubiKeyProvider.isChallengeResponseConfigured(this.slot, this.serial)) {
|
|
2378
|
+
throw new Error(
|
|
2379
|
+
`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.`
|
|
2380
|
+
);
|
|
2381
|
+
}
|
|
2382
|
+
if (!force && await this.keyExists(this.encryptedKeyPath)) {
|
|
2383
|
+
throw new Error(
|
|
2384
|
+
`Encrypted key file already exists: ${this.encryptedKeyPath}. Use force: true to overwrite.`
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
let serial;
|
|
2388
|
+
if (this.serial) {
|
|
2389
|
+
serial = this.serial;
|
|
2390
|
+
} else {
|
|
2391
|
+
const devices = await _YubiKeyProvider.listDevices();
|
|
2392
|
+
if (devices.length === 1 && devices[0]) {
|
|
2393
|
+
serial = devices[0].serial;
|
|
2394
|
+
} else if (devices.length > 1) {
|
|
2395
|
+
console.warn(
|
|
2396
|
+
"WARNING: Multiple YubiKeys detected but no serial specified. Key will not be bound to a specific device. For better security, specify a serial number."
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
|
|
2401
|
+
const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
2402
|
+
try {
|
|
2403
|
+
await generateKeyPair({
|
|
2404
|
+
privatePath: tempPrivateKeyPath,
|
|
2405
|
+
publicPath: publicKeyPath,
|
|
2406
|
+
force
|
|
2407
|
+
});
|
|
2408
|
+
const privateKeyContent = await fs8__namespace.readFile(tempPrivateKeyPath, "utf8");
|
|
2409
|
+
const challenge = crypto3__namespace.randomBytes(32);
|
|
2410
|
+
const salt = crypto3__namespace.randomBytes(32);
|
|
2411
|
+
const iv = crypto3__namespace.randomBytes(12);
|
|
2412
|
+
const response = await performChallengeResponse(challenge, this.slot, this.serial);
|
|
2413
|
+
const aesKey = deriveKey(response, salt);
|
|
2414
|
+
const aad = constructAAD(1, this.slot, serial);
|
|
2415
|
+
const cipher = crypto3__namespace.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
2416
|
+
cipher.setAAD(aad);
|
|
2417
|
+
const ciphertext = Buffer.concat([
|
|
2418
|
+
cipher.update(Buffer.from(privateKeyContent, "utf8")),
|
|
2419
|
+
cipher.final()
|
|
2420
|
+
]);
|
|
2421
|
+
const authTag = cipher.getAuthTag();
|
|
2422
|
+
const keyFile = {
|
|
2423
|
+
version: 1,
|
|
2424
|
+
iv: iv.toString("base64"),
|
|
2425
|
+
authTag: authTag.toString("base64"),
|
|
2426
|
+
salt: salt.toString("base64"),
|
|
2427
|
+
challenge: challenge.toString("base64"),
|
|
2428
|
+
ciphertext: ciphertext.toString("base64"),
|
|
2429
|
+
slot: this.slot,
|
|
2430
|
+
aad: aad.toString("base64"),
|
|
2431
|
+
...serial && { serial }
|
|
2432
|
+
};
|
|
2433
|
+
await fs8__namespace.mkdir(path2__namespace.dirname(this.encryptedKeyPath), { recursive: true });
|
|
2434
|
+
await fs8__namespace.writeFile(this.encryptedKeyPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
|
|
2435
|
+
await setKeyPermissions(this.encryptedKeyPath);
|
|
2436
|
+
const keySize = Buffer.byteLength(privateKeyContent);
|
|
2437
|
+
await fs8__namespace.writeFile(tempPrivateKeyPath, crypto3__namespace.randomBytes(keySize));
|
|
2438
|
+
await fs8__namespace.unlink(tempPrivateKeyPath);
|
|
2439
|
+
await fs8__namespace.rmdir(tempDir);
|
|
2440
|
+
return {
|
|
2441
|
+
privateKeyRef: this.encryptedKeyPath,
|
|
2442
|
+
publicKeyPath,
|
|
2443
|
+
storageDescription: `YubiKey-encrypted: ${this.encryptedKeyPath}`
|
|
2444
|
+
};
|
|
2445
|
+
} catch (error) {
|
|
2446
|
+
try {
|
|
2447
|
+
await fs8__namespace.rm(tempDir, { recursive: true, force: true });
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
throw error;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Encrypt an existing private key with YubiKey challenge-response.
|
|
2455
|
+
*
|
|
2456
|
+
* @remarks
|
|
2457
|
+
* This static method allows encrypting a private key that was generated
|
|
2458
|
+
* elsewhere (e.g., by the CLI) without having to create a provider instance first.
|
|
2459
|
+
*
|
|
2460
|
+
* **Security Note**: Always specify a serial number to bind the key to a specific YubiKey.
|
|
2461
|
+
* The serial provides defense-in-depth by ensuring only the intended YubiKey can decrypt.
|
|
2462
|
+
*
|
|
2463
|
+
* @param options - Encryption options
|
|
2464
|
+
* @returns Path to the encrypted key file and storage description
|
|
2465
|
+
* @public
|
|
2466
|
+
*/
|
|
2467
|
+
static async encryptPrivateKey(options) {
|
|
2468
|
+
const { privateKey, encryptedKeyPath, slot = 2, serial } = options;
|
|
2469
|
+
const resolvedPath = path2__namespace.resolve(encryptedKeyPath);
|
|
2470
|
+
const configDir = getAttestItConfigDir();
|
|
2471
|
+
if (!resolvedPath.startsWith(configDir)) {
|
|
2472
|
+
throw new Error(
|
|
2473
|
+
`Encrypted key path must be within attest-it config directory (${configDir}). Got: ${resolvedPath}`
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
if (!serial) {
|
|
2477
|
+
console.warn(
|
|
2478
|
+
"WARNING: No YubiKey serial number specified. Key will not be bound to a specific device. For better security, specify a serial number."
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2481
|
+
if (!await _YubiKeyProvider.isChallengeResponseConfigured(slot, serial)) {
|
|
2482
|
+
throw new Error(
|
|
2483
|
+
`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.`
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
const challenge = crypto3__namespace.randomBytes(32);
|
|
2487
|
+
const salt = crypto3__namespace.randomBytes(32);
|
|
2488
|
+
const iv = crypto3__namespace.randomBytes(12);
|
|
2489
|
+
const response = await performChallengeResponse(challenge, slot, serial);
|
|
2490
|
+
const aesKey = deriveKey(response, salt);
|
|
2491
|
+
const aad = constructAAD(1, slot, serial);
|
|
2492
|
+
const cipher = crypto3__namespace.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
2493
|
+
cipher.setAAD(aad);
|
|
2494
|
+
const ciphertext = Buffer.concat([
|
|
2495
|
+
cipher.update(Buffer.from(privateKey, "utf8")),
|
|
2496
|
+
cipher.final()
|
|
2497
|
+
]);
|
|
2498
|
+
const authTag = cipher.getAuthTag();
|
|
2499
|
+
const keyFile = {
|
|
2500
|
+
version: 1,
|
|
2501
|
+
iv: iv.toString("base64"),
|
|
2502
|
+
authTag: authTag.toString("base64"),
|
|
2503
|
+
salt: salt.toString("base64"),
|
|
2504
|
+
challenge: challenge.toString("base64"),
|
|
2505
|
+
ciphertext: ciphertext.toString("base64"),
|
|
2506
|
+
slot,
|
|
2507
|
+
aad: aad.toString("base64"),
|
|
2508
|
+
...serial && { serial }
|
|
2509
|
+
};
|
|
2510
|
+
await fs8__namespace.mkdir(path2__namespace.dirname(resolvedPath), { recursive: true });
|
|
2511
|
+
await fs8__namespace.writeFile(resolvedPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
|
|
2512
|
+
await setKeyPermissions(resolvedPath);
|
|
2513
|
+
return {
|
|
2514
|
+
encryptedKeyPath: resolvedPath,
|
|
2515
|
+
storageDescription: `YubiKey-encrypted: ${resolvedPath}`
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Get the configuration for this provider.
|
|
2520
|
+
*/
|
|
2521
|
+
getConfig() {
|
|
2522
|
+
return {
|
|
2523
|
+
type: this.type,
|
|
2524
|
+
options: {
|
|
2525
|
+
encryptedKeyPath: this.encryptedKeyPath,
|
|
2526
|
+
slot: this.slot,
|
|
2527
|
+
...this.serial && { serial: this.serial }
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
async function execCommand3(command, args) {
|
|
2533
|
+
return new Promise((resolve4, reject) => {
|
|
2534
|
+
const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2535
|
+
let stdout = "";
|
|
2536
|
+
let stderr = "";
|
|
2537
|
+
proc.stdout.on("data", (data) => {
|
|
2538
|
+
stdout += data.toString();
|
|
2539
|
+
});
|
|
2540
|
+
proc.stderr.on("data", (data) => {
|
|
2541
|
+
stderr += data.toString();
|
|
2542
|
+
});
|
|
2543
|
+
proc.on("close", (code) => {
|
|
2544
|
+
if (code === 0) {
|
|
2545
|
+
resolve4(stdout.trim());
|
|
2546
|
+
} else {
|
|
2547
|
+
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
proc.on("error", (error) => {
|
|
2551
|
+
reject(error);
|
|
2552
|
+
});
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
async function performChallengeResponse(challenge, slot, serial) {
|
|
2556
|
+
const args = ["otp", "chalresp", "--slot", String(slot)];
|
|
2557
|
+
if (serial) {
|
|
2558
|
+
args.unshift("--device", serial);
|
|
2559
|
+
}
|
|
2560
|
+
args.push(challenge.toString("hex"));
|
|
2561
|
+
try {
|
|
2562
|
+
const output = await execCommand3("ykman", args);
|
|
2563
|
+
return Buffer.from(output.trim(), "hex");
|
|
2564
|
+
} catch {
|
|
2565
|
+
throw new Error(
|
|
2566
|
+
"YubiKey challenge-response failed. Verify your YubiKey is inserted and the slot is configured for challenge-response."
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function deriveKey(response, salt) {
|
|
2571
|
+
const derived = crypto3__namespace.hkdfSync("sha256", response, salt, "attest-it-yubikey-v1", 32);
|
|
2572
|
+
return Buffer.from(derived);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// src/key-provider/registry.ts
|
|
2576
|
+
var KeyProviderRegistry = class {
|
|
2577
|
+
static providers = /* @__PURE__ */ new Map();
|
|
2578
|
+
/**
|
|
2579
|
+
* Register a key provider factory.
|
|
2580
|
+
* @param type - Provider type identifier
|
|
2581
|
+
* @param factory - Factory function to create provider instances
|
|
2582
|
+
*/
|
|
2583
|
+
static register(type, factory) {
|
|
2584
|
+
this.providers.set(type, factory);
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Create a key provider from configuration.
|
|
2588
|
+
* @param config - Provider configuration
|
|
2589
|
+
* @returns A key provider instance
|
|
2590
|
+
* @throws Error if the provider type is not registered
|
|
2591
|
+
*/
|
|
2592
|
+
static create(config) {
|
|
2593
|
+
const factory = this.providers.get(config.type);
|
|
2594
|
+
if (!factory) {
|
|
2595
|
+
throw new Error(
|
|
2596
|
+
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
2597
|
+
);
|
|
2598
|
+
}
|
|
2599
|
+
return factory(config);
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Get all registered provider types.
|
|
2603
|
+
* @returns Array of provider type identifiers
|
|
2604
|
+
*/
|
|
2605
|
+
static getProviderTypes() {
|
|
2606
|
+
return Array.from(this.providers.keys());
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
KeyProviderRegistry.register("filesystem", (config) => {
|
|
2610
|
+
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
2611
|
+
if (privateKeyPath !== void 0) {
|
|
2612
|
+
return new FilesystemKeyProvider({ privateKeyPath });
|
|
2613
|
+
}
|
|
2614
|
+
return new FilesystemKeyProvider();
|
|
2615
|
+
});
|
|
2616
|
+
KeyProviderRegistry.register("1password", (config) => {
|
|
2617
|
+
const { options } = config;
|
|
2618
|
+
const account = typeof options.account === "string" ? options.account : void 0;
|
|
2619
|
+
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
2620
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
2621
|
+
if (!vault || !itemName) {
|
|
2622
|
+
throw new Error("1Password provider requires vault and itemName options");
|
|
2623
|
+
}
|
|
2624
|
+
if (account !== void 0) {
|
|
2625
|
+
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
2626
|
+
}
|
|
2627
|
+
return new OnePasswordKeyProvider({ vault, itemName });
|
|
2628
|
+
});
|
|
2629
|
+
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
2630
|
+
const { options } = config;
|
|
2631
|
+
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
2632
|
+
if (!itemName) {
|
|
2633
|
+
throw new Error("macOS Keychain provider requires itemName option");
|
|
2634
|
+
}
|
|
2635
|
+
return new MacOSKeychainKeyProvider({ itemName });
|
|
2636
|
+
});
|
|
2637
|
+
KeyProviderRegistry.register("yubikey", (config) => {
|
|
2638
|
+
const { options } = config;
|
|
2639
|
+
const encryptedKeyPath = typeof options.encryptedKeyPath === "string" ? options.encryptedKeyPath : "";
|
|
2640
|
+
if (!encryptedKeyPath) {
|
|
2641
|
+
throw new Error("YubiKey provider requires encryptedKeyPath option");
|
|
2642
|
+
}
|
|
2643
|
+
const slot = typeof options.slot === "number" && (options.slot === 1 || options.slot === 2) ? options.slot : void 0;
|
|
2644
|
+
const serial = typeof options.serial === "string" ? options.serial : void 0;
|
|
2645
|
+
const providerOptions = {
|
|
2646
|
+
encryptedKeyPath
|
|
2647
|
+
};
|
|
2648
|
+
if (slot !== void 0) {
|
|
2649
|
+
providerOptions.slot = slot;
|
|
2650
|
+
}
|
|
2651
|
+
if (serial !== void 0) {
|
|
2652
|
+
providerOptions.serial = serial;
|
|
2653
|
+
}
|
|
2654
|
+
return new YubiKeyProvider(providerOptions);
|
|
2655
|
+
});
|
|
2656
|
+
var cliExperienceSchema = zod.z.object({
|
|
2657
|
+
declinedCompletionInstall: zod.z.boolean().optional()
|
|
2658
|
+
}).strict();
|
|
2659
|
+
var userPreferencesSchema = zod.z.object({
|
|
2660
|
+
cliExperience: cliExperienceSchema.optional()
|
|
2661
|
+
}).strict();
|
|
2662
|
+
function getPreferencesPath() {
|
|
2663
|
+
return path2.join(getAttestItConfigDir(), "preferences.yaml");
|
|
2664
|
+
}
|
|
2665
|
+
async function loadPreferences() {
|
|
2666
|
+
const prefsPath = getPreferencesPath();
|
|
2667
|
+
try {
|
|
2668
|
+
const content = await fs8.readFile(prefsPath, "utf8");
|
|
2669
|
+
const parsed = yaml.parse(content);
|
|
2670
|
+
const result = userPreferencesSchema.safeParse(parsed);
|
|
2671
|
+
if (result.success) {
|
|
2672
|
+
const prefs = {};
|
|
2673
|
+
if (result.data.cliExperience) {
|
|
2674
|
+
prefs.cliExperience = {
|
|
2675
|
+
...result.data.cliExperience.declinedCompletionInstall !== void 0 && {
|
|
2676
|
+
declinedCompletionInstall: result.data.cliExperience.declinedCompletionInstall
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
return prefs;
|
|
2681
|
+
}
|
|
2682
|
+
console.warn("Invalid preferences file, using defaults:", result.error.message);
|
|
2683
|
+
return {};
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
if (error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
2686
|
+
return {};
|
|
2687
|
+
}
|
|
2688
|
+
throw error;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
async function savePreferences(preferences) {
|
|
2692
|
+
const prefsPath = getPreferencesPath();
|
|
2693
|
+
const content = yaml.stringify(preferences);
|
|
2694
|
+
const dir = path2.dirname(prefsPath);
|
|
2695
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
2696
|
+
await fs8.writeFile(prefsPath, content, "utf8");
|
|
2697
|
+
}
|
|
2698
|
+
async function setPreference(key, value) {
|
|
2699
|
+
const prefs = await loadPreferences();
|
|
2700
|
+
prefs[key] = value;
|
|
2701
|
+
await savePreferences(prefs);
|
|
2702
|
+
}
|
|
2703
|
+
async function getPreference(key) {
|
|
2704
|
+
const prefs = await loadPreferences();
|
|
2705
|
+
return prefs[key];
|
|
2706
|
+
}
|
|
1863
2707
|
function isAuthorizedSigner(config, gateId, publicKey) {
|
|
1864
2708
|
const gate = config.gates?.[gateId];
|
|
1865
2709
|
if (!gate) {
|
|
@@ -2183,7 +3027,10 @@ exports.KeyProviderRegistry = KeyProviderRegistry;
|
|
|
2183
3027
|
exports.LocalConfigValidationError = LocalConfigValidationError;
|
|
2184
3028
|
exports.MacOSKeychainKeyProvider = MacOSKeychainKeyProvider;
|
|
2185
3029
|
exports.OnePasswordKeyProvider = OnePasswordKeyProvider;
|
|
3030
|
+
exports.OperationalValidationError = OperationalValidationError;
|
|
3031
|
+
exports.PolicyValidationError = PolicyValidationError;
|
|
2186
3032
|
exports.SignatureInvalidError = SignatureInvalidError;
|
|
3033
|
+
exports.YubiKeyProvider = YubiKeyProvider;
|
|
2187
3034
|
exports.canonicalizeAttestations = canonicalizeAttestations;
|
|
2188
3035
|
exports.checkOpenSSL = checkOpenSSL;
|
|
2189
3036
|
exports.computeFingerprint = computeFingerprint;
|
|
@@ -2203,6 +3050,8 @@ exports.getDefaultPrivateKeyPath = getDefaultPrivateKeyPath;
|
|
|
2203
3050
|
exports.getDefaultPublicKeyPath = getDefaultPublicKeyPath;
|
|
2204
3051
|
exports.getGate = getGate;
|
|
2205
3052
|
exports.getLocalConfigPath = getLocalConfigPath;
|
|
3053
|
+
exports.getPreference = getPreference;
|
|
3054
|
+
exports.getPreferencesPath = getPreferencesPath;
|
|
2206
3055
|
exports.getPublicKeyFromPrivate = getPublicKeyFromPrivate;
|
|
2207
3056
|
exports.isAuthorizedSigner = isAuthorizedSigner;
|
|
2208
3057
|
exports.listPackageFiles = listPackageFiles;
|
|
@@ -2210,7 +3059,13 @@ exports.loadConfig = loadConfig;
|
|
|
2210
3059
|
exports.loadConfigSync = loadConfigSync;
|
|
2211
3060
|
exports.loadLocalConfig = loadLocalConfig;
|
|
2212
3061
|
exports.loadLocalConfigSync = loadLocalConfigSync;
|
|
3062
|
+
exports.loadPreferences = loadPreferences;
|
|
3063
|
+
exports.mergeConfigs = mergeConfigs;
|
|
3064
|
+
exports.operationalSchema = operationalSchema;
|
|
2213
3065
|
exports.parseDuration = parseDuration;
|
|
3066
|
+
exports.parseOperationalContent = parseOperationalContent;
|
|
3067
|
+
exports.parsePolicyContent = parsePolicyContent;
|
|
3068
|
+
exports.policySchema = policySchema;
|
|
2214
3069
|
exports.readAndVerifyAttestations = readAndVerifyAttestations;
|
|
2215
3070
|
exports.readAttestations = readAttestations;
|
|
2216
3071
|
exports.readAttestationsSync = readAttestationsSync;
|
|
@@ -2220,12 +3075,15 @@ exports.removeAttestation = removeAttestation;
|
|
|
2220
3075
|
exports.resolveConfigPaths = resolveConfigPaths;
|
|
2221
3076
|
exports.saveLocalConfig = saveLocalConfig;
|
|
2222
3077
|
exports.saveLocalConfigSync = saveLocalConfigSync;
|
|
3078
|
+
exports.savePreferences = savePreferences;
|
|
2223
3079
|
exports.setAttestItHomeDir = setAttestItHomeDir;
|
|
2224
3080
|
exports.setKeyPermissions = setKeyPermissions;
|
|
3081
|
+
exports.setPreference = setPreference;
|
|
2225
3082
|
exports.sign = sign;
|
|
2226
3083
|
exports.signEd25519 = sign3;
|
|
2227
3084
|
exports.toAttestItConfig = toAttestItConfig;
|
|
2228
3085
|
exports.upsertAttestation = upsertAttestation;
|
|
3086
|
+
exports.validateSuiteGateReferences = validateSuiteGateReferences;
|
|
2229
3087
|
exports.verify = verify;
|
|
2230
3088
|
exports.verifyAllSeals = verifyAllSeals;
|
|
2231
3089
|
exports.verifyAttestations = verifyAttestations;
|