@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/index.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  var child_process = require('child_process');
4
- var fs2 = require('fs/promises');
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 crypto2 = require('crypto');
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 fs2__namespace = /*#__PURE__*/_interopNamespace(fs2);
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 crypto2__namespace = /*#__PURE__*/_interopNamespace(crypto2);
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((resolve3, reject) => {
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
- resolve3({
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 fs2__namespace.mkdir(dirPath, { recursive: true });
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 fs2__namespace.access(filePath);
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 fs2__namespace.unlink(filePath);
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 fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
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 fs2__namespace.writeFile(dataFile, processBuffer);
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 fs2__namespace.readFile(sigFile);
225
+ const sigBuffer = await fs8__namespace.readFile(sigFile);
226
226
  return sigBuffer.toString("base64");
227
227
  } finally {
228
228
  try {
229
- await fs2__namespace.rm(tmpDir, { recursive: true, force: true });
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 fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
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 fs2__namespace.writeFile(dataFile, processBuffer);
253
- await fs2__namespace.writeFile(sigFile, sigBuffer);
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 fs2__namespace.rm(tmpDir, { recursive: true, force: true });
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 fs2__namespace.chmod(keyPath, 384);
274
+ await fs8__namespace.chmod(keyPath, 384);
275
275
  } else {
276
- await fs2__namespace.chmod(keyPath, 384);
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 fs2.readFile(resolvedPath, "utf8");
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 = crypto2__namespace.createHash("sha256").update(concatenated).digest();
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((resolve3, reject) => {
549
- const hash2 = crypto2__namespace.createHash("sha256");
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
- resolve3(hash2.digest());
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 = crypto2__namespace.createHash("sha256");
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 = crypto2__namespace.createHash("sha256");
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 = crypto2__namespace.generateKeyPairSync("ed25519", {
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 = crypto2__namespace.createPublicKey(publicKey);
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 = crypto2__namespace.createPrivateKey(privateKeyPem);
917
- const signatureResult = crypto2__namespace.sign(null, dataBuffer, privateKeyObj);
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 = crypto2__namespace.createPublicKey({
1250
+ const publicKeyObj = crypto3__namespace.createPublicKey({
958
1251
  key: spkiBuffer,
959
1252
  format: "der",
960
1253
  type: "spki"
961
1254
  });
962
- return crypto2__namespace.verify(null, dataBuffer, publicKeyObj, signatureBuffer);
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 = crypto2__namespace.createPrivateKey(privateKeyPem);
975
- const publicKeyObj = crypto2__namespace.createPublicKey(privateKeyObj);
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 fs2__namespace.access(keyRef);
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 fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
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 fs2__namespace.unlink(tempKeyPath);
1319
- await fs2__namespace.rmdir(tempDir);
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 fs2__namespace.rm(tempDir, { recursive: true, force: true });
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 fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
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 fs2__namespace.unlink(tempPrivateKeyPath);
1367
- await fs2__namespace.rmdir(tempDir);
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 fs2__namespace.rm(tempDir, { recursive: true, force: true });
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((resolve3, reject) => {
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
- resolve3(stdout.trim());
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 fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
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 fs2__namespace.writeFile(tempKeyPath, keyContent, { mode: 384 });
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 fs2__namespace.unlink(tempKeyPath);
1530
- await fs2__namespace.rmdir(tempDir);
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 fs2__namespace.rm(tempDir, { recursive: true, force: true });
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 fs2__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
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 fs2__namespace.readFile(tempPrivateKeyPath, "utf8");
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 fs2__namespace.unlink(tempPrivateKeyPath);
1583
- await fs2__namespace.rmdir(tempDir);
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 fs2__namespace.rm(tempDir, { recursive: true, force: true });
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((resolve3, reject) => {
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
- resolve3(stdout.trim());
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/registry.ts
1637
- var KeyProviderRegistry = class {
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 fs2.readFile(resolvedPath, "utf8");
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 fs2.mkdir(dir, { recursive: true });
1851
- await fs2.writeFile(resolvedPath, content, "utf8");
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;