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":