diff --git a/doc/keystore.md b/doc/keystore.md index 621f302a..55da5961 100644 --- a/doc/keystore.md +++ b/doc/keystore.md @@ -101,27 +101,55 @@ See [WireGuard VPN](vpn-wireguard.md) for PSK generation and usage examples. ## Viewing Keys -
admin@example:/> configure
-admin@example:/config/> show keystore
-asymmetric-keys {
- asymmetric-key genkey {
- public-key-format ssh-public-key-format;
- public-key MIIBCgKCAQEAm6uCENSafz7mIfIJ8O.... AQAB;
- private-key-format rsa-private-key-format;
- cleartext-private-key MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYw...b7dyPr4mpHg==;
- }
-}
+The `show keystore` command in admin-exec mode gives an overview of all
+keys in the keystore. Passphrases (WiFi passwords) are decoded and shown
+in cleartext, while binary keys (WireGuard PSKs) are shown as base64:
+
+admin@example:/> show keystore
+────────────────────────────────────────────────────────────────────────
+Symmetric Keys
+NAME FORMAT VALUE
+my-wifi-key passphrase MySecretPassword
+wg-psk octet-string zYr83O4Ykj9i1gN+/aaosJxQx...
+
+────────────────────────────────────────────────────────────────────────
+Asymmetric Keys
+NAME TYPE PUBLIC KEY
+genkey rsa MIIBCgKCAQEAnj0YinjhYDgYbEGuh7...
+wg-tunnel x25519 bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1...
+
+
+To see the full (untruncated) details of a specific key, use the
+`symmetric` or `asymmetric` qualifier with the key name:
+
+admin@example:/> show keystore symmetric my-wifi-key
+name : my-wifi-key
+format : passphrase
+value : MySecretPassword
+
+admin@example:/> show keystore asymmetric genkey
+name : genkey
+algorithm : rsa
+public key format : ssh-public-key
+public key : MIIBCgKCAQEAnj0YinjhY...full key...IDAQAB
+
+
+> [!NOTE]
+> The `show keystore` command is protected by NACM. Only users in the
+> `admin` group can view keystore data. Operator-level users will see a
+> message indicating that no keystore data is available.
+
+The full configuration-mode view (including private keys) is still
+available via `configure` and then `show keystore`:
+
+admin@example:/config/> show keystore
> [!WARNING]
-> The `show keystore` command displays private keys in cleartext. Be careful
-> when viewing keys on shared screens or in logged sessions.
-
-To list only asymmetric or symmetric keys:
-
-admin@example:/config/> show keystore asymmetric-keys
-admin@example:/config/> show keystore symmetric-keys
-
+> The configuration-mode `show keystore` displays private keys in
+> cleartext. Be careful when viewing keys on shared screens or in
+> logged sessions. The admin-exec `show keystore` command never
+> displays private keys.
## Deleting Keys
diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py
index 339e8181..f86a164a 100755
--- a/src/bin/show/__init__.py
+++ b/src/bin/show/__init__.py
@@ -659,6 +659,25 @@ def nacm(args: List[str]) -> None:
print(f"Unknown NACM subcommand: {subcommand}")
+def keystore(args: List[str]) -> None:
+ data = get_json("/ietf-keystore:keystore", "running", quiet=True)
+ if not data:
+ user = os.environ.get('USER', 'unknown')
+ print(f'No keystore data available (check NACM permissions for "{user}").')
+ return
+
+ if RAW_OUTPUT:
+ print(json.dumps(data, indent=2))
+ return
+
+ if len(args) == 0 or not args[0]:
+ cli_pretty(data, "show-keystore")
+ elif len(args) >= 2 and args[0] in ("symmetric", "asymmetric"):
+ cli_pretty(data, "show-keystore", "-t", args[0], "-n", args[1])
+ else:
+ print("Usage: show keystore [symmetric | asymmetric ]")
+
+
def execute_command(command: str, args: List[str]):
command_mapping = {
'bfd': bfd,
@@ -667,6 +686,7 @@ def execute_command(command: str, args: List[str]):
'dhcp': dhcp,
'hardware': hardware,
'interface': interface,
+ 'keystore': keystore,
'lldp': lldp,
'nacm': nacm,
'ntp': ntp,
diff --git a/src/klish-plugin-infix/src/infix.c b/src/klish-plugin-infix/src/infix.c
index a6515c21..48a2106d 100644
--- a/src/klish-plugin-infix/src/infix.c
+++ b/src/klish-plugin-infix/src/infix.c
@@ -413,6 +413,20 @@ int infix_groups(kcontext_t *ctx)
"| jq -r '.\"ietf-netconf-acm:nacm\".groups.group[].name'");
}
+int infix_sym_keys(kcontext_t *ctx)
+{
+ (void)ctx;
+ return shellf("copy running -x /ietf-keystore:keystore/symmetric-keys "
+ "| jq -r '.\"ietf-keystore:keystore\".\"symmetric-keys\".\"symmetric-key\"[].name'");
+}
+
+int infix_asym_keys(kcontext_t *ctx)
+{
+ (void)ctx;
+ return shellf("copy running -x /ietf-keystore:keystore/asymmetric-keys "
+ "| jq -r '.\"ietf-keystore:keystore\".\"asymmetric-keys\".\"asymmetric-key\"[].name'");
+}
+
int kplugin_infix_fini(kcontext_t *ctx)
{
(void)ctx;
@@ -432,6 +446,8 @@ int kplugin_infix_init(kcontext_t *ctx)
kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces));
kplugin_add_syms(plugin, ksym_new("users", infix_users));
kplugin_add_syms(plugin, ksym_new("groups", infix_groups));
+ kplugin_add_syms(plugin, ksym_new("sym_keys", infix_sym_keys));
+ kplugin_add_syms(plugin, ksym_new("asym_keys", infix_asym_keys));
kplugin_add_syms(plugin, ksym_new("firewall_zones", infix_firewall_zones));
kplugin_add_syms(plugin, ksym_new("firewall_policies", infix_firewall_policies));
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml
index 5a66eec5..bce73835 100644
--- a/src/klish-plugin-infix/xml/infix.xml
+++ b/src/klish-plugin-infix/xml/infix.xml
@@ -155,6 +155,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -353,6 +367,20 @@ echo "Public: $pub"
+
+
+
+
+ show keystore symmetric $KLISH_PARAM_name
+
+
+
+ show keystore asymmetric $KLISH_PARAM_name
+
+
+ show keystore
+
+
diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py
index ffd381a4..488655d7 100755
--- a/src/statd/python/cli_pretty/cli_pretty.py
+++ b/src/statd/python/cli_pretty/cli_pretty.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
+import base64
import json
import argparse
import sys
@@ -1030,6 +1031,7 @@ class DhcpServer:
class Iface:
# Class variable to hold routing-enabled interfaces for the current display session
_routing_ifaces = set()
+ _keystore = {}
def __init__(self, data):
self.data = data
@@ -3381,6 +3383,131 @@ def show_nacm_user(json):
print("For detailed rules, use: show nacm group ")
+def _keystore_format_name(key_format):
+ """Simplify key-format to a short display name."""
+ fmt = key_format.split(':')[-1] if ':' in key_format else key_format
+ return fmt.replace('-key-format', '')
+
+
+def _keystore_decode_symmetric(key):
+ """Decode a symmetric key value for display."""
+ key_format = key.get('key-format', '')
+ fmt = _keystore_format_name(key_format)
+ b64val = key.get('cleartext-symmetric-key', '')
+
+ if fmt == 'passphrase' and b64val:
+ try:
+ return base64.b64decode(b64val).decode('utf-8')
+ except Exception:
+ return b64val
+ return b64val if b64val else '-'
+
+
+def _keystore_find_key(keystore, kind, name):
+ """Find a key by type and name in the keystore."""
+ if kind == 'symmetric':
+ keys = keystore.get('symmetric-keys', {}).get('symmetric-key', [])
+ else:
+ keys = keystore.get('asymmetric-keys', {}).get('asymmetric-key', [])
+ for key in keys:
+ if key.get('name') == name:
+ return key
+ return None
+
+
+def _keystore_asym_type(key):
+ """Derive asymmetric key algorithm from key format fields."""
+ for field in ('private-key-format', 'public-key-format'):
+ fmt = key.get(field, '')
+ name = fmt.split(':')[-1] if ':' in fmt else fmt
+ name = name.replace('-private-key-format', '').replace('-public-key-format', '')
+ if name:
+ return name
+ return ''
+
+
+def show_keystore_detail(keystore, kind, name):
+ """Display detailed information about a specific key."""
+ key = _keystore_find_key(keystore, kind, name)
+ if not key:
+ print(f'{kind.capitalize()} key "{name}" not found.')
+ return
+
+ print(f"{'name':<{20}}: {name}")
+ if kind == 'symmetric':
+ fmt = _keystore_format_name(key.get('key-format', ''))
+ value = _keystore_decode_symmetric(key)
+ print(f"{'format':<{20}}: {fmt}")
+ print(f"{'value':<{20}}: {value}")
+ else:
+ ktype = _keystore_asym_type(key)
+ if ktype:
+ print(f"{'algorithm':<{20}}: {ktype}")
+ pub_fmt = _keystore_format_name(key.get('public-key-format', ''))
+ if pub_fmt:
+ print(f"{'public key format':<{20}}: {pub_fmt}")
+ pub_key = key.get('public-key', '')
+ if pub_key:
+ print(f"{'public key':<{20}}: {pub_key}")
+
+
+def show_keystore(json, kind=None, name=None):
+ """Display keystore keys overview or detail for a specific key."""
+ keystore = json.get("ietf-keystore:keystore", {})
+ if not keystore:
+ print("Keystore is empty.")
+ return
+
+ if kind and name:
+ show_keystore_detail(keystore, kind, name)
+ return
+
+ TABLE_WIDTH = 72
+
+ # Symmetric keys
+ sym_keys_data = keystore.get('symmetric-keys', {}).get('symmetric-key', [])
+ if sym_keys_data:
+ Decore.title("Symmetric Keys", TABLE_WIDTH)
+ table = SimpleTable([
+ Column('NAME', flexible=True),
+ Column('FORMAT'),
+ Column('VALUE', flexible=True)
+ ], min_width=TABLE_WIDTH)
+
+ for key in sym_keys_data:
+ name = key.get('name', '')
+ fmt = _keystore_format_name(key.get('key-format', ''))
+ value = _keystore_decode_symmetric(key)
+ table.row(name, fmt, value)
+
+ table.print()
+
+ # Asymmetric keys
+ asym_keys_data = keystore.get('asymmetric-keys', {}).get('asymmetric-key', [])
+ if asym_keys_data:
+ Decore.title("Asymmetric Keys", TABLE_WIDTH)
+ table = SimpleTable([
+ Column('NAME', flexible=True),
+ Column('TYPE'),
+ Column('PUBLIC KEY', flexible=True)
+ ], min_width=TABLE_WIDTH)
+
+ for key in asym_keys_data:
+ name = key.get('name', '')
+ ktype = _keystore_asym_type(key)
+
+ pub_key = key.get('public-key', '')
+ if len(pub_key) > 40:
+ pub_key = pub_key[:37] + '...'
+
+ table.row(name, ktype, pub_key)
+
+ table.print()
+
+ if not sym_keys_data and not asym_keys_data:
+ print("Keystore is empty.")
+
+
def show_system(json):
"""System information overivew"""
if not json.get("ietf-system:system-state"):
@@ -5369,6 +5496,10 @@ def main():
subparsers.add_parser('show-nacm-group', help='Show NACM group details')
subparsers.add_parser('show-nacm-user', help='Show NACM user details')
+ ks_parser = subparsers.add_parser('show-keystore', help='Show keystore keys')
+ ks_parser.add_argument('-t', '--type', help='Key type (symmetric or asymmetric)')
+ ks_parser.add_argument('-n', '--name', help='Key name')
+
subparsers.add_parser('show-ntp', help='Show NTP status') \
.add_argument('-a', '--address', help='Show details for specific address')
subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status')
@@ -5438,6 +5569,8 @@ def main():
show_nacm_group(json_data)
elif args.command == "show-nacm-user":
show_nacm_user(json_data)
+ elif args.command == "show-keystore":
+ show_keystore(json_data, getattr(args, 'type', None), args.name)
elif args.command == "show-ntp":
show_ntp(json_data, args.address)
elif args.command == "show-ntp-tracking":