@attest-it/cli 0.6.1 → 0.7.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.
@@ -6,7 +6,7 @@ import * as path from 'path';
6
6
  import { join, dirname } from 'path';
7
7
  import { detectTheme } from 'chromaterm';
8
8
  import { input, select, confirm, checkbox } from '@inquirer/prompts';
9
- import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync, checkOpenSSL, getDefaultPublicKeyPath, OnePasswordKeyProvider, MacOSKeychainKeyProvider, generateKeyPair, setKeyPermissions, getGate, loadLocalConfig, YubiKeyProvider, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, findConfigPath, loadPreferences, savePreferences, findAttestation, setAttestItHomeDir } from '@attest-it/core';
9
+ import { loadConfig, toAttestItConfig, readSealsSync, computeFingerprintSync, verifyGateSeal, verifyAllSeals, computeFingerprint, createAttestation, readAttestations, upsertAttestation, KeyProviderRegistry, getDefaultPrivateKeyPath, FilesystemKeyProvider, writeSignedAttestations, loadLocalConfigSync, getActiveIdentity, isAuthorizedSigner, createSeal, writeSealsSync, checkOpenSSL, getDefaultPublicKeyPath, OnePasswordKeyProvider, MacOSKeychainKeyProvider, generateKeyPair, setKeyPermissions, getGate, loadLocalConfig, YubiKeyProvider, getAttestItConfigDir, generateEd25519KeyPair, saveLocalConfig, savePublicKey, findConfigPath, loadPreferences, savePreferences, findAttestation, setAttestItHomeDir, getDefaultYubiKeyEncryptedKeyPath } from '@attest-it/core';
10
10
  import tabtab2 from '@pnpm/tabtab';
11
11
  import { spawn } from 'child_process';
12
12
  import * as os from 'os';
@@ -1639,17 +1639,28 @@ function getKeyRefFromIdentity(identity) {
1639
1639
  }
1640
1640
  }
1641
1641
  function KeygenInteractive(props) {
1642
- const { onComplete, onError } = props;
1642
+ const { onComplete, onCancel, onError } = props;
1643
1643
  const [step, setStep] = useState("checking-providers");
1644
1644
  const [opAvailable, setOpAvailable] = useState(false);
1645
1645
  const [keychainAvailable, setKeychainAvailable] = useState(false);
1646
+ const [yubiKeyAvailable, setYubiKeyAvailable] = useState(false);
1646
1647
  const [accounts, setAccounts] = useState([]);
1647
1648
  const [vaults, setVaults] = useState([]);
1649
+ const [yubiKeyDevices, setYubiKeyDevices] = useState([]);
1648
1650
  const [_selectedProvider, setSelectedProvider] = useState();
1649
1651
  const [selectedAccount, setSelectedAccount] = useState();
1650
1652
  const [selectedVault, setSelectedVault] = useState();
1651
1653
  const [itemName, setItemName] = useState("attest-it-private-key");
1652
1654
  const [keychainItemName, setKeychainItemName] = useState("attest-it-private-key");
1655
+ const [selectedYubiKeySerial, setSelectedYubiKeySerial] = useState();
1656
+ const [selectedYubiKeySlot, setSelectedYubiKeySlot] = useState(2);
1657
+ const [slot1Configured, setSlot1Configured] = useState(false);
1658
+ const [slot2Configured, setSlot2Configured] = useState(false);
1659
+ useInput((_input, key) => {
1660
+ if (key.escape) {
1661
+ onCancel();
1662
+ }
1663
+ });
1653
1664
  useEffect(() => {
1654
1665
  const checkProviders = async () => {
1655
1666
  try {
@@ -1664,6 +1675,19 @@ function KeygenInteractive(props) {
1664
1675
  }
1665
1676
  const isKeychainAvailable = MacOSKeychainKeyProvider.isAvailable();
1666
1677
  setKeychainAvailable(isKeychainAvailable);
1678
+ try {
1679
+ const isInstalled = await YubiKeyProvider.isInstalled();
1680
+ if (isInstalled) {
1681
+ const isConnected = await YubiKeyProvider.isConnected();
1682
+ if (isConnected) {
1683
+ const devices = await YubiKeyProvider.listDevices();
1684
+ setYubiKeyDevices(devices);
1685
+ setYubiKeyAvailable(devices.length > 0);
1686
+ }
1687
+ }
1688
+ } catch {
1689
+ setYubiKeyAvailable(false);
1690
+ }
1667
1691
  setStep("select-provider");
1668
1692
  };
1669
1693
  void checkProviders();
@@ -1681,6 +1705,27 @@ function KeygenInteractive(props) {
1681
1705
  void fetchVaults();
1682
1706
  }
1683
1707
  }, [step, selectedAccount, onError]);
1708
+ const checkYubiKeySlots = async (serial) => {
1709
+ try {
1710
+ const slot1 = await YubiKeyProvider.isChallengeResponseConfigured(1, serial);
1711
+ const slot2 = await YubiKeyProvider.isChallengeResponseConfigured(2, serial);
1712
+ setSlot1Configured(slot1);
1713
+ setSlot2Configured(slot2);
1714
+ if (slot1 && slot2) {
1715
+ setStep("select-yubikey-slot");
1716
+ } else if (slot2) {
1717
+ setSelectedYubiKeySlot(2);
1718
+ void generateKeys("yubikey");
1719
+ } else if (slot1) {
1720
+ setSelectedYubiKeySlot(1);
1721
+ void generateKeys("yubikey");
1722
+ } else {
1723
+ setStep("yubikey-offer-setup");
1724
+ }
1725
+ } catch (err) {
1726
+ onError(err instanceof Error ? err : new Error("Failed to check YubiKey slots"));
1727
+ }
1728
+ };
1684
1729
  const handleProviderSelect = (value) => {
1685
1730
  if (value === "filesystem") {
1686
1731
  setSelectedProvider("filesystem");
@@ -1696,6 +1741,14 @@ function KeygenInteractive(props) {
1696
1741
  } else if (value === "macos-keychain") {
1697
1742
  setSelectedProvider("macos-keychain");
1698
1743
  setStep("enter-keychain-item-name");
1744
+ } else if (value === "yubikey") {
1745
+ setSelectedProvider("yubikey");
1746
+ if (yubiKeyDevices.length > 1) {
1747
+ setStep("select-yubikey-device");
1748
+ } else if (yubiKeyDevices.length === 1 && yubiKeyDevices[0]) {
1749
+ setSelectedYubiKeySerial(yubiKeyDevices[0].serial);
1750
+ void checkYubiKeySlots(yubiKeyDevices[0].serial);
1751
+ }
1699
1752
  }
1700
1753
  };
1701
1754
  const handleAccountSelect = (value) => {
@@ -1714,6 +1767,47 @@ function KeygenInteractive(props) {
1714
1767
  setKeychainItemName(value);
1715
1768
  void generateKeys("macos-keychain");
1716
1769
  };
1770
+ const handleYubiKeyDeviceSelect = (value) => {
1771
+ setSelectedYubiKeySerial(value);
1772
+ void checkYubiKeySlots(value);
1773
+ };
1774
+ const handleYubiKeySlotSelect = (value) => {
1775
+ const slot = value === "1" ? 1 : 2;
1776
+ setSelectedYubiKeySlot(slot);
1777
+ void generateKeys("yubikey");
1778
+ };
1779
+ const handleYubiKeySetupConfirm = (value) => {
1780
+ if (value === "yes") {
1781
+ void setupYubiKeySlot();
1782
+ } else {
1783
+ onError(new Error("YubiKey setup cancelled"));
1784
+ }
1785
+ };
1786
+ const setupYubiKeySlot = async () => {
1787
+ setStep("yubikey-configuring");
1788
+ try {
1789
+ const { spawn: spawn3 } = await import('child_process');
1790
+ const args = ["otp", "chalresp", "--touch", "--generate", "2"];
1791
+ if (selectedYubiKeySerial) {
1792
+ args.unshift("--device", selectedYubiKeySerial);
1793
+ }
1794
+ await new Promise((resolve2, reject) => {
1795
+ const proc = spawn3("ykman", args, { stdio: "inherit" });
1796
+ proc.on("close", (code) => {
1797
+ if (code === 0) {
1798
+ resolve2();
1799
+ } else {
1800
+ reject(new Error(`ykman exited with code ${String(code)}`));
1801
+ }
1802
+ });
1803
+ proc.on("error", reject);
1804
+ });
1805
+ setSelectedYubiKeySlot(2);
1806
+ void generateKeys("yubikey");
1807
+ } catch (err) {
1808
+ onError(err instanceof Error ? err : new Error("Failed to configure YubiKey"));
1809
+ }
1810
+ };
1717
1811
  const generateKeys = async (provider) => {
1718
1812
  setStep("generating");
1719
1813
  try {
@@ -1764,7 +1858,7 @@ function KeygenInteractive(props) {
1764
1858
  completionResult.account = selectedAccount;
1765
1859
  }
1766
1860
  onComplete(completionResult);
1767
- } else {
1861
+ } else if (provider === "macos-keychain") {
1768
1862
  if (!keychainItemName) {
1769
1863
  throw new Error("Item name is required for macOS Keychain");
1770
1864
  }
@@ -1783,6 +1877,33 @@ function KeygenInteractive(props) {
1783
1877
  storageDescription: result.storageDescription,
1784
1878
  itemName: keychainItemName
1785
1879
  });
1880
+ } else {
1881
+ const encryptedKeyPath = getDefaultYubiKeyEncryptedKeyPath();
1882
+ const providerOptions = {
1883
+ encryptedKeyPath,
1884
+ slot: selectedYubiKeySlot
1885
+ };
1886
+ if (selectedYubiKeySerial !== void 0) {
1887
+ providerOptions.serial = selectedYubiKeySerial;
1888
+ }
1889
+ const ykProvider = new YubiKeyProvider(providerOptions);
1890
+ const genOptions = { publicKeyPath };
1891
+ if (props.force !== void 0) {
1892
+ genOptions.force = props.force;
1893
+ }
1894
+ const result = await ykProvider.generateKeyPair(genOptions);
1895
+ const completionResult = {
1896
+ provider: "yubikey",
1897
+ publicKeyPath: result.publicKeyPath,
1898
+ privateKeyRef: result.privateKeyRef,
1899
+ storageDescription: result.storageDescription,
1900
+ slot: selectedYubiKeySlot,
1901
+ encryptedKeyPath
1902
+ };
1903
+ if (selectedYubiKeySerial !== void 0) {
1904
+ completionResult.serial = selectedYubiKeySerial;
1905
+ }
1906
+ onComplete(completionResult);
1786
1907
  }
1787
1908
  setStep("done");
1788
1909
  } catch (err) {
@@ -1808,6 +1929,12 @@ function KeygenInteractive(props) {
1808
1929
  value: "macos-keychain"
1809
1930
  });
1810
1931
  }
1932
+ if (yubiKeyAvailable) {
1933
+ options.push({
1934
+ label: "YubiKey (hardware security key)",
1935
+ value: "yubikey"
1936
+ });
1937
+ }
1811
1938
  if (opAvailable) {
1812
1939
  options.push({
1813
1940
  label: "1Password (requires op CLI)",
@@ -1822,7 +1949,7 @@ function KeygenInteractive(props) {
1822
1949
  }
1823
1950
  if (step === "select-account") {
1824
1951
  const options = accounts.map((account) => ({
1825
- label: account.email,
1952
+ label: account.name ? `${account.name} (${account.email})` : account.email,
1826
1953
  value: account.user_uuid
1827
1954
  }));
1828
1955
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
@@ -1864,6 +1991,65 @@ function KeygenInteractive(props) {
1864
1991
  /* @__PURE__ */ jsx(TextInput, { defaultValue: keychainItemName, onSubmit: handleKeychainItemNameSubmit })
1865
1992
  ] });
1866
1993
  }
1994
+ if (step === "select-yubikey-device") {
1995
+ const options = yubiKeyDevices.map((device) => ({
1996
+ label: `${device.type} (Serial: ${device.serial})`,
1997
+ value: device.serial
1998
+ }));
1999
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2000
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Select YubiKey device:" }),
2001
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2002
+ /* @__PURE__ */ jsx(Select, { options, onChange: handleYubiKeyDeviceSelect })
2003
+ ] });
2004
+ }
2005
+ if (step === "select-yubikey-slot") {
2006
+ const options = [];
2007
+ if (slot2Configured) {
2008
+ options.push({
2009
+ label: "Slot 2 (recommended)",
2010
+ value: "2"
2011
+ });
2012
+ }
2013
+ if (slot1Configured) {
2014
+ options.push({
2015
+ label: "Slot 1",
2016
+ value: "1"
2017
+ });
2018
+ }
2019
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2020
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Select YubiKey slot for challenge-response:" }),
2021
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2022
+ /* @__PURE__ */ jsx(Select, { options, onChange: handleYubiKeySlotSelect })
2023
+ ] });
2024
+ }
2025
+ if (step === "yubikey-offer-setup") {
2026
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2027
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Your YubiKey is not configured for challenge-response." }),
2028
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2029
+ /* @__PURE__ */ jsx(Text, { children: "Would you like to configure slot 2 for challenge-response now?" }),
2030
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "This will enable touch-to-sign functionality." }),
2031
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "" }),
2032
+ /* @__PURE__ */ jsx(
2033
+ Select,
2034
+ {
2035
+ options: [
2036
+ { label: "Yes, configure my YubiKey", value: "yes" },
2037
+ { label: "No, cancel", value: "no" }
2038
+ ],
2039
+ onChange: handleYubiKeySetupConfirm
2040
+ }
2041
+ )
2042
+ ] });
2043
+ }
2044
+ if (step === "yubikey-configuring") {
2045
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
2046
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
2047
+ /* @__PURE__ */ jsx(Spinner, {}),
2048
+ /* @__PURE__ */ jsx(Text, { children: "Configuring YubiKey slot 2 for challenge-response..." })
2049
+ ] }),
2050
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Touch your YubiKey when it flashes." })
2051
+ ] });
2052
+ }
1867
2053
  if (step === "generating") {
1868
2054
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1869
2055
  /* @__PURE__ */ jsx(Spinner, {}),
@@ -2948,6 +3134,7 @@ async function runCreate() {
2948
3134
  };
2949
3135
  }
2950
3136
  await saveLocalConfig(newConfig);
3137
+ const publicKeyResult = await savePublicKey(slug, keyPair.publicKey);
2951
3138
  log("");
2952
3139
  success("Identity created successfully");
2953
3140
  log("");
@@ -2962,6 +3149,12 @@ async function runCreate() {
2962
3149
  log(` Public Key: ${keyPair.publicKey.slice(0, 32)}...`);
2963
3150
  log(` Private Key: ${keyStorageDescription}`);
2964
3151
  log("");
3152
+ log(theme3.blue.bold()("Public key saved to:"));
3153
+ log(` ${publicKeyResult.homePath}`);
3154
+ if (publicKeyResult.projectPath) {
3155
+ log(` ${publicKeyResult.projectPath}`);
3156
+ }
3157
+ log("");
2965
3158
  if (!existingConfig) {
2966
3159
  success(`Set as active identity`);
2967
3160
  log("");