@attest-it/cli 0.6.0 → 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");
@@ -1688,7 +1733,7 @@ function KeygenInteractive(props) {
1688
1733
  } else if (value === "1password") {
1689
1734
  setSelectedProvider("1password");
1690
1735
  if (accounts.length === 1 && accounts[0]) {
1691
- setSelectedAccount(accounts[0].email);
1736
+ setSelectedAccount(accounts[0].user_uuid);
1692
1737
  setStep("select-vault");
1693
1738
  } else {
1694
1739
  setStep("select-account");
@@ -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 {
@@ -1735,8 +1829,12 @@ function KeygenInteractive(props) {
1735
1829
  if (!selectedVault || !itemName) {
1736
1830
  throw new Error("Vault and item name are required for 1Password");
1737
1831
  }
1832
+ const vault = vaults.find((v) => v.id === selectedVault);
1833
+ if (!vault) {
1834
+ throw new Error("Selected vault not found");
1835
+ }
1738
1836
  const providerOptions = {
1739
- vault: selectedVault,
1837
+ vault: vault.name,
1740
1838
  itemName
1741
1839
  };
1742
1840
  if (selectedAccount !== void 0) {
@@ -1753,14 +1851,14 @@ function KeygenInteractive(props) {
1753
1851
  publicKeyPath: result.publicKeyPath,
1754
1852
  privateKeyRef: result.privateKeyRef,
1755
1853
  storageDescription: result.storageDescription,
1756
- vault: selectedVault,
1854
+ vault: vault.name,
1757
1855
  itemName
1758
1856
  };
1759
1857
  if (selectedAccount !== void 0) {
1760
1858
  completionResult.account = selectedAccount;
1761
1859
  }
1762
1860
  onComplete(completionResult);
1763
- } else {
1861
+ } else if (provider === "macos-keychain") {
1764
1862
  if (!keychainItemName) {
1765
1863
  throw new Error("Item name is required for macOS Keychain");
1766
1864
  }
@@ -1779,6 +1877,33 @@ function KeygenInteractive(props) {
1779
1877
  storageDescription: result.storageDescription,
1780
1878
  itemName: keychainItemName
1781
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);
1782
1907
  }
1783
1908
  setStep("done");
1784
1909
  } catch (err) {
@@ -1804,6 +1929,12 @@ function KeygenInteractive(props) {
1804
1929
  value: "macos-keychain"
1805
1930
  });
1806
1931
  }
1932
+ if (yubiKeyAvailable) {
1933
+ options.push({
1934
+ label: "YubiKey (hardware security key)",
1935
+ value: "yubikey"
1936
+ });
1937
+ }
1807
1938
  if (opAvailable) {
1808
1939
  options.push({
1809
1940
  label: "1Password (requires op CLI)",
@@ -1818,8 +1949,8 @@ function KeygenInteractive(props) {
1818
1949
  }
1819
1950
  if (step === "select-account") {
1820
1951
  const options = accounts.map((account) => ({
1821
- label: account.email,
1822
- value: account.email
1952
+ label: account.name ? `${account.name} (${account.email})` : account.email,
1953
+ value: account.user_uuid
1823
1954
  }));
1824
1955
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1825
1956
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Select 1Password account:" }),
@@ -1836,7 +1967,7 @@ function KeygenInteractive(props) {
1836
1967
  }
1837
1968
  const options = vaults.map((vault) => ({
1838
1969
  label: vault.name,
1839
- value: vault.name
1970
+ value: vault.id
1840
1971
  }));
1841
1972
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1842
1973
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Select vault for private key storage:" }),
@@ -1860,6 +1991,65 @@ function KeygenInteractive(props) {
1860
1991
  /* @__PURE__ */ jsx(TextInput, { defaultValue: keychainItemName, onSubmit: handleKeychainItemNameSubmit })
1861
1992
  ] });
1862
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
+ }
1863
2053
  if (step === "generating") {
1864
2054
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { flexDirection: "row", gap: 1, children: [
1865
2055
  /* @__PURE__ */ jsx(Spinner, {}),
@@ -2944,6 +3134,7 @@ async function runCreate() {
2944
3134
  };
2945
3135
  }
2946
3136
  await saveLocalConfig(newConfig);
3137
+ const publicKeyResult = await savePublicKey(slug, keyPair.publicKey);
2947
3138
  log("");
2948
3139
  success("Identity created successfully");
2949
3140
  log("");
@@ -2958,6 +3149,12 @@ async function runCreate() {
2958
3149
  log(` Public Key: ${keyPair.publicKey.slice(0, 32)}...`);
2959
3150
  log(` Private Key: ${keyStorageDescription}`);
2960
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("");
2961
3158
  if (!existingConfig) {
2962
3159
  success(`Set as active identity`);
2963
3160
  log("");