@attest-it/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core-alpha.d.ts +1241 -523
- package/dist/core-beta.d.ts +1241 -523
- package/dist/core-public.d.ts +1241 -523
- package/dist/core-unstripped.d.ts +1241 -523
- package/dist/index.cjs +1057 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +791 -2
- package/dist/index.d.ts +787 -2
- package/dist/index.js +1019 -116
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.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
|
}
|
|
@@ -1425,6 +1718,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1425
1718
|
type = "macos-keychain";
|
|
1426
1719
|
displayName = "macOS Keychain";
|
|
1427
1720
|
itemName;
|
|
1721
|
+
keychain;
|
|
1428
1722
|
static ACCOUNT = "attest-it";
|
|
1429
1723
|
/**
|
|
1430
1724
|
* Create a new MacOSKeychainKeyProvider.
|
|
@@ -1432,6 +1726,9 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1432
1726
|
*/
|
|
1433
1727
|
constructor(options) {
|
|
1434
1728
|
this.itemName = options.itemName;
|
|
1729
|
+
if (options.keychain !== void 0) {
|
|
1730
|
+
this.keychain = options.keychain;
|
|
1731
|
+
}
|
|
1435
1732
|
}
|
|
1436
1733
|
/**
|
|
1437
1734
|
* Check if this provider is available.
|
|
@@ -1440,6 +1737,32 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1440
1737
|
static isAvailable() {
|
|
1441
1738
|
return process.platform === "darwin";
|
|
1442
1739
|
}
|
|
1740
|
+
/**
|
|
1741
|
+
* List available keychains on the system.
|
|
1742
|
+
* @returns Array of keychain information
|
|
1743
|
+
*/
|
|
1744
|
+
static async listKeychains() {
|
|
1745
|
+
if (!_MacOSKeychainKeyProvider.isAvailable()) {
|
|
1746
|
+
return [];
|
|
1747
|
+
}
|
|
1748
|
+
try {
|
|
1749
|
+
const output = await execCommand2("security", ["list-keychains"]);
|
|
1750
|
+
const keychains = [];
|
|
1751
|
+
const lines = output.split("\n");
|
|
1752
|
+
for (const line of lines) {
|
|
1753
|
+
const match = /"(.+)"/.exec(line.trim());
|
|
1754
|
+
if (match?.[1]) {
|
|
1755
|
+
const fullPath = match[1];
|
|
1756
|
+
const filename = fullPath.split("/").pop() ?? fullPath;
|
|
1757
|
+
const name = filename.replace(/\.keychain(-db)?$/, "");
|
|
1758
|
+
keychains.push({ path: fullPath, name });
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return keychains;
|
|
1762
|
+
} catch {
|
|
1763
|
+
return [];
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1443
1766
|
/**
|
|
1444
1767
|
* Check if this provider is available on the current system.
|
|
1445
1768
|
*/
|
|
@@ -1452,13 +1775,11 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1452
1775
|
*/
|
|
1453
1776
|
async keyExists(keyRef) {
|
|
1454
1777
|
try {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
keyRef
|
|
1461
|
-
]);
|
|
1778
|
+
const args = ["find-generic-password", "-a", _MacOSKeychainKeyProvider.ACCOUNT, "-s", keyRef];
|
|
1779
|
+
if (this.keychain) {
|
|
1780
|
+
args.push(this.keychain);
|
|
1781
|
+
}
|
|
1782
|
+
await execCommand2("security", args);
|
|
1462
1783
|
return true;
|
|
1463
1784
|
} catch {
|
|
1464
1785
|
return false;
|
|
@@ -1476,26 +1797,30 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1476
1797
|
`Key not found in macOS Keychain: "${keyRef}" (account: ${_MacOSKeychainKeyProvider.ACCOUNT})`
|
|
1477
1798
|
);
|
|
1478
1799
|
}
|
|
1479
|
-
const tempDir = await
|
|
1800
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-"));
|
|
1480
1801
|
const tempKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1481
1802
|
try {
|
|
1482
|
-
const
|
|
1803
|
+
const findArgs = [
|
|
1483
1804
|
"find-generic-password",
|
|
1484
1805
|
"-a",
|
|
1485
1806
|
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
1486
1807
|
"-s",
|
|
1487
1808
|
keyRef,
|
|
1488
1809
|
"-w"
|
|
1489
|
-
]
|
|
1810
|
+
];
|
|
1811
|
+
if (this.keychain) {
|
|
1812
|
+
findArgs.push(this.keychain);
|
|
1813
|
+
}
|
|
1814
|
+
const base64Key = await execCommand2("security", findArgs);
|
|
1490
1815
|
const keyContent = Buffer.from(base64Key, "base64").toString("utf8");
|
|
1491
|
-
await
|
|
1816
|
+
await fs8__namespace.writeFile(tempKeyPath, keyContent, { mode: 384 });
|
|
1492
1817
|
await setKeyPermissions(tempKeyPath);
|
|
1493
1818
|
return {
|
|
1494
1819
|
keyPath: tempKeyPath,
|
|
1495
1820
|
cleanup: async () => {
|
|
1496
1821
|
try {
|
|
1497
|
-
await
|
|
1498
|
-
await
|
|
1822
|
+
await fs8__namespace.unlink(tempKeyPath);
|
|
1823
|
+
await fs8__namespace.rmdir(tempDir);
|
|
1499
1824
|
} catch (cleanupError) {
|
|
1500
1825
|
console.warn(
|
|
1501
1826
|
`Warning: Failed to clean up temporary key file at ${tempKeyPath}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1505,7 +1830,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1505
1830
|
};
|
|
1506
1831
|
} catch (error) {
|
|
1507
1832
|
try {
|
|
1508
|
-
await
|
|
1833
|
+
await fs8__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1509
1834
|
} catch (cleanupError) {
|
|
1510
1835
|
console.warn(
|
|
1511
1836
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1521,7 +1846,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1521
1846
|
*/
|
|
1522
1847
|
async generateKeyPair(options) {
|
|
1523
1848
|
const { publicKeyPath, force = false } = options;
|
|
1524
|
-
const tempDir = await
|
|
1849
|
+
const tempDir = await fs8__namespace.mkdtemp(path2__namespace.join(os__namespace.tmpdir(), "attest-it-keygen-"));
|
|
1525
1850
|
const tempPrivateKeyPath = path2__namespace.join(tempDir, "private.pem");
|
|
1526
1851
|
try {
|
|
1527
1852
|
await generateKeyPair({
|
|
@@ -1529,9 +1854,9 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1529
1854
|
publicPath: publicKeyPath,
|
|
1530
1855
|
force
|
|
1531
1856
|
});
|
|
1532
|
-
const privateKeyContent = await
|
|
1857
|
+
const privateKeyContent = await fs8__namespace.readFile(tempPrivateKeyPath, "utf8");
|
|
1533
1858
|
const base64Key = Buffer.from(privateKeyContent, "utf8").toString("base64");
|
|
1534
|
-
|
|
1859
|
+
const addArgs = [
|
|
1535
1860
|
"add-generic-password",
|
|
1536
1861
|
"-a",
|
|
1537
1862
|
_MacOSKeychainKeyProvider.ACCOUNT,
|
|
@@ -1542,9 +1867,13 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1542
1867
|
"-T",
|
|
1543
1868
|
"",
|
|
1544
1869
|
"-U"
|
|
1545
|
-
]
|
|
1546
|
-
|
|
1547
|
-
|
|
1870
|
+
];
|
|
1871
|
+
if (this.keychain) {
|
|
1872
|
+
addArgs.push(this.keychain);
|
|
1873
|
+
}
|
|
1874
|
+
await execCommand2("security", addArgs);
|
|
1875
|
+
await fs8__namespace.unlink(tempPrivateKeyPath);
|
|
1876
|
+
await fs8__namespace.rmdir(tempDir);
|
|
1548
1877
|
return {
|
|
1549
1878
|
privateKeyRef: this.itemName,
|
|
1550
1879
|
publicKeyPath,
|
|
@@ -1552,7 +1881,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1552
1881
|
};
|
|
1553
1882
|
} catch (error) {
|
|
1554
1883
|
try {
|
|
1555
|
-
await
|
|
1884
|
+
await fs8__namespace.rm(tempDir, { recursive: true, force: true });
|
|
1556
1885
|
} catch (cleanupError) {
|
|
1557
1886
|
console.warn(
|
|
1558
1887
|
`Warning: Failed to clean up temporary key directory at ${tempDir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
|
|
@@ -1574,7 +1903,7 @@ var MacOSKeychainKeyProvider = class _MacOSKeychainKeyProvider {
|
|
|
1574
1903
|
}
|
|
1575
1904
|
};
|
|
1576
1905
|
async function execCommand2(command, args) {
|
|
1577
|
-
return new Promise((
|
|
1906
|
+
return new Promise((resolve4, reject) => {
|
|
1578
1907
|
const proc = child_process.spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1579
1908
|
let stdout = "";
|
|
1580
1909
|
let stderr = "";
|
|
@@ -1586,7 +1915,7 @@ async function execCommand2(command, args) {
|
|
|
1586
1915
|
});
|
|
1587
1916
|
proc.on("close", (code) => {
|
|
1588
1917
|
if (code === 0) {
|
|
1589
|
-
|
|
1918
|
+
resolve4(stdout.trim());
|
|
1590
1919
|
} else {
|
|
1591
1920
|
reject(new Error(`Command failed with exit code ${String(code)}: ${stderr}`));
|
|
1592
1921
|
}
|
|
@@ -1597,68 +1926,15 @@ async function execCommand2(command, args) {
|
|
|
1597
1926
|
});
|
|
1598
1927
|
}
|
|
1599
1928
|
|
|
1600
|
-
// src/key-provider/
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
this.providers.set(type, factory);
|
|
1610
|
-
}
|
|
1611
|
-
/**
|
|
1612
|
-
* Create a key provider from configuration.
|
|
1613
|
-
* @param config - Provider configuration
|
|
1614
|
-
* @returns A key provider instance
|
|
1615
|
-
* @throws Error if the provider type is not registered
|
|
1616
|
-
*/
|
|
1617
|
-
static create(config) {
|
|
1618
|
-
const factory = this.providers.get(config.type);
|
|
1619
|
-
if (!factory) {
|
|
1620
|
-
throw new Error(
|
|
1621
|
-
`Unknown key provider type: ${config.type}. Available types: ${Array.from(this.providers.keys()).join(", ")}`
|
|
1622
|
-
);
|
|
1623
|
-
}
|
|
1624
|
-
return factory(config);
|
|
1625
|
-
}
|
|
1626
|
-
/**
|
|
1627
|
-
* Get all registered provider types.
|
|
1628
|
-
* @returns Array of provider type identifiers
|
|
1629
|
-
*/
|
|
1630
|
-
static getProviderTypes() {
|
|
1631
|
-
return Array.from(this.providers.keys());
|
|
1632
|
-
}
|
|
1633
|
-
};
|
|
1634
|
-
KeyProviderRegistry.register("filesystem", (config) => {
|
|
1635
|
-
const privateKeyPath = typeof config.options.privateKeyPath === "string" ? config.options.privateKeyPath : void 0;
|
|
1636
|
-
if (privateKeyPath !== void 0) {
|
|
1637
|
-
return new FilesystemKeyProvider({ privateKeyPath });
|
|
1638
|
-
}
|
|
1639
|
-
return new FilesystemKeyProvider();
|
|
1640
|
-
});
|
|
1641
|
-
KeyProviderRegistry.register("1password", (config) => {
|
|
1642
|
-
const { options } = config;
|
|
1643
|
-
const account = typeof options.account === "string" ? options.account : void 0;
|
|
1644
|
-
const vault = typeof options.vault === "string" ? options.vault : "";
|
|
1645
|
-
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1646
|
-
if (!vault || !itemName) {
|
|
1647
|
-
throw new Error("1Password provider requires vault and itemName options");
|
|
1648
|
-
}
|
|
1649
|
-
if (account !== void 0) {
|
|
1650
|
-
return new OnePasswordKeyProvider({ account, vault, itemName });
|
|
1651
|
-
}
|
|
1652
|
-
return new OnePasswordKeyProvider({ vault, itemName });
|
|
1653
|
-
});
|
|
1654
|
-
KeyProviderRegistry.register("macos-keychain", (config) => {
|
|
1655
|
-
const { options } = config;
|
|
1656
|
-
const itemName = typeof options.itemName === "string" ? options.itemName : "";
|
|
1657
|
-
if (!itemName) {
|
|
1658
|
-
throw new Error("macOS Keychain provider requires itemName option");
|
|
1659
|
-
}
|
|
1660
|
-
return new MacOSKeychainKeyProvider({ itemName });
|
|
1661
|
-
});
|
|
1929
|
+
// src/key-provider/yubikey-provider.ts
|
|
1930
|
+
init_crypto();
|
|
1931
|
+
var homeDirOverride = null;
|
|
1932
|
+
function setAttestItHomeDir(dir) {
|
|
1933
|
+
homeDirOverride = dir;
|
|
1934
|
+
}
|
|
1935
|
+
function getAttestItHomeDir() {
|
|
1936
|
+
return homeDirOverride;
|
|
1937
|
+
}
|
|
1662
1938
|
var privateKeyRefSchema = zod.z.discriminatedUnion("type", [
|
|
1663
1939
|
zod.z.object({
|
|
1664
1940
|
type: zod.z.literal("file"),
|
|
@@ -1667,7 +1943,8 @@ var privateKeyRefSchema = zod.z.discriminatedUnion("type", [
|
|
|
1667
1943
|
zod.z.object({
|
|
1668
1944
|
type: zod.z.literal("keychain"),
|
|
1669
1945
|
service: zod.z.string().min(1, "Service name cannot be empty"),
|
|
1670
|
-
account: zod.z.string().min(1, "Account name cannot be empty")
|
|
1946
|
+
account: zod.z.string().min(1, "Account name cannot be empty"),
|
|
1947
|
+
keychain: zod.z.string().optional()
|
|
1671
1948
|
}),
|
|
1672
1949
|
zod.z.object({
|
|
1673
1950
|
type: zod.z.literal("1password"),
|
|
@@ -1698,9 +1975,18 @@ var LocalConfigValidationError = class extends Error {
|
|
|
1698
1975
|
}
|
|
1699
1976
|
};
|
|
1700
1977
|
function getLocalConfigPath() {
|
|
1978
|
+
if (homeDirOverride) {
|
|
1979
|
+
return path2.join(homeDirOverride, "config.yaml");
|
|
1980
|
+
}
|
|
1701
1981
|
const home = os.homedir();
|
|
1702
1982
|
return path2.join(home, ".config", "attest-it", "config.yaml");
|
|
1703
1983
|
}
|
|
1984
|
+
function getAttestItConfigDir() {
|
|
1985
|
+
if (homeDirOverride) {
|
|
1986
|
+
return homeDirOverride;
|
|
1987
|
+
}
|
|
1988
|
+
return path2.join(os.homedir(), ".config", "attest-it");
|
|
1989
|
+
}
|
|
1704
1990
|
function parseLocalConfigContent(content) {
|
|
1705
1991
|
let rawConfig;
|
|
1706
1992
|
try {
|
|
@@ -1731,6 +2017,15 @@ function parseLocalConfigContent(content) {
|
|
|
1731
2017
|
},
|
|
1732
2018
|
...identity.privateKey.field !== void 0 && { field: identity.privateKey.field }
|
|
1733
2019
|
};
|
|
2020
|
+
} else if (identity.privateKey.type === "keychain") {
|
|
2021
|
+
privateKey = {
|
|
2022
|
+
type: "keychain",
|
|
2023
|
+
service: identity.privateKey.service,
|
|
2024
|
+
account: identity.privateKey.account,
|
|
2025
|
+
...identity.privateKey.keychain !== void 0 && {
|
|
2026
|
+
keychain: identity.privateKey.keychain
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
1734
2029
|
} else {
|
|
1735
2030
|
privateKey = identity.privateKey;
|
|
1736
2031
|
}
|
|
@@ -1754,7 +2049,7 @@ function parseLocalConfigContent(content) {
|
|
|
1754
2049
|
async function loadLocalConfig(configPath) {
|
|
1755
2050
|
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1756
2051
|
try {
|
|
1757
|
-
const content = await
|
|
2052
|
+
const content = await fs8.readFile(resolvedPath, "utf8");
|
|
1758
2053
|
return parseLocalConfigContent(content);
|
|
1759
2054
|
} catch (error) {
|
|
1760
2055
|
if (error instanceof LocalConfigValidationError) {
|
|
@@ -1785,8 +2080,8 @@ async function saveLocalConfig(config, configPath) {
|
|
|
1785
2080
|
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
1786
2081
|
const content = yaml.stringify(config);
|
|
1787
2082
|
const dir = path2.dirname(resolvedPath);
|
|
1788
|
-
await
|
|
1789
|
-
await
|
|
2083
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
2084
|
+
await fs8.writeFile(resolvedPath, content, "utf8");
|
|
1790
2085
|
}
|
|
1791
2086
|
function saveLocalConfigSync(config, configPath) {
|
|
1792
2087
|
const resolvedPath = configPath ?? getLocalConfigPath();
|
|
@@ -1798,6 +2093,617 @@ function saveLocalConfigSync(config, configPath) {
|
|
|
1798
2093
|
function getActiveIdentity(config) {
|
|
1799
2094
|
return config.identities[config.activeIdentity];
|
|
1800
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
|
+
}
|
|
1801
2707
|
function isAuthorizedSigner(config, gateId, publicKey) {
|
|
1802
2708
|
const gate = config.gates?.[gateId];
|
|
1803
2709
|
if (!gate) {
|
|
@@ -2121,7 +3027,10 @@ exports.KeyProviderRegistry = KeyProviderRegistry;
|
|
|
2121
3027
|
exports.LocalConfigValidationError = LocalConfigValidationError;
|
|
2122
3028
|
exports.MacOSKeychainKeyProvider = MacOSKeychainKeyProvider;
|
|
2123
3029
|
exports.OnePasswordKeyProvider = OnePasswordKeyProvider;
|
|
3030
|
+
exports.OperationalValidationError = OperationalValidationError;
|
|
3031
|
+
exports.PolicyValidationError = PolicyValidationError;
|
|
2124
3032
|
exports.SignatureInvalidError = SignatureInvalidError;
|
|
3033
|
+
exports.YubiKeyProvider = YubiKeyProvider;
|
|
2125
3034
|
exports.canonicalizeAttestations = canonicalizeAttestations;
|
|
2126
3035
|
exports.checkOpenSSL = checkOpenSSL;
|
|
2127
3036
|
exports.computeFingerprint = computeFingerprint;
|
|
@@ -2134,11 +3043,15 @@ exports.findTeamMemberByPublicKey = findTeamMemberByPublicKey;
|
|
|
2134
3043
|
exports.generateEd25519KeyPair = generateKeyPair2;
|
|
2135
3044
|
exports.generateKeyPair = generateKeyPair;
|
|
2136
3045
|
exports.getActiveIdentity = getActiveIdentity;
|
|
3046
|
+
exports.getAttestItConfigDir = getAttestItConfigDir;
|
|
3047
|
+
exports.getAttestItHomeDir = getAttestItHomeDir;
|
|
2137
3048
|
exports.getAuthorizedSignersForGate = getAuthorizedSignersForGate;
|
|
2138
3049
|
exports.getDefaultPrivateKeyPath = getDefaultPrivateKeyPath;
|
|
2139
3050
|
exports.getDefaultPublicKeyPath = getDefaultPublicKeyPath;
|
|
2140
3051
|
exports.getGate = getGate;
|
|
2141
3052
|
exports.getLocalConfigPath = getLocalConfigPath;
|
|
3053
|
+
exports.getPreference = getPreference;
|
|
3054
|
+
exports.getPreferencesPath = getPreferencesPath;
|
|
2142
3055
|
exports.getPublicKeyFromPrivate = getPublicKeyFromPrivate;
|
|
2143
3056
|
exports.isAuthorizedSigner = isAuthorizedSigner;
|
|
2144
3057
|
exports.listPackageFiles = listPackageFiles;
|
|
@@ -2146,7 +3059,13 @@ exports.loadConfig = loadConfig;
|
|
|
2146
3059
|
exports.loadConfigSync = loadConfigSync;
|
|
2147
3060
|
exports.loadLocalConfig = loadLocalConfig;
|
|
2148
3061
|
exports.loadLocalConfigSync = loadLocalConfigSync;
|
|
3062
|
+
exports.loadPreferences = loadPreferences;
|
|
3063
|
+
exports.mergeConfigs = mergeConfigs;
|
|
3064
|
+
exports.operationalSchema = operationalSchema;
|
|
2149
3065
|
exports.parseDuration = parseDuration;
|
|
3066
|
+
exports.parseOperationalContent = parseOperationalContent;
|
|
3067
|
+
exports.parsePolicyContent = parsePolicyContent;
|
|
3068
|
+
exports.policySchema = policySchema;
|
|
2150
3069
|
exports.readAndVerifyAttestations = readAndVerifyAttestations;
|
|
2151
3070
|
exports.readAttestations = readAttestations;
|
|
2152
3071
|
exports.readAttestationsSync = readAttestationsSync;
|
|
@@ -2156,11 +3075,15 @@ exports.removeAttestation = removeAttestation;
|
|
|
2156
3075
|
exports.resolveConfigPaths = resolveConfigPaths;
|
|
2157
3076
|
exports.saveLocalConfig = saveLocalConfig;
|
|
2158
3077
|
exports.saveLocalConfigSync = saveLocalConfigSync;
|
|
3078
|
+
exports.savePreferences = savePreferences;
|
|
3079
|
+
exports.setAttestItHomeDir = setAttestItHomeDir;
|
|
2159
3080
|
exports.setKeyPermissions = setKeyPermissions;
|
|
3081
|
+
exports.setPreference = setPreference;
|
|
2160
3082
|
exports.sign = sign;
|
|
2161
3083
|
exports.signEd25519 = sign3;
|
|
2162
3084
|
exports.toAttestItConfig = toAttestItConfig;
|
|
2163
3085
|
exports.upsertAttestation = upsertAttestation;
|
|
3086
|
+
exports.validateSuiteGateReferences = validateSuiteGateReferences;
|
|
2164
3087
|
exports.verify = verify;
|
|
2165
3088
|
exports.verifyAllSeals = verifyAllSeals;
|
|
2166
3089
|
exports.verifyAttestations = verifyAttestations;
|