Merge pull request #1379 from kernelkit/copilot/add-infix-keystore-documentation

Keystore refactor and documentation

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-02-03 08:09:05 +01:00
committed by GitHub
28 changed files with 950 additions and 169 deletions
@@ -218,8 +218,8 @@
"symmetric-key": [
{
"name": "wifi",
"infix-keystore:symmetric-key": "infixinfix",
"infix-keystore:key-format": "infix-crypto-types:wifi-preshared-key-format"
"cleartext-symmetric-key": "aW5maXhpbmZpeA==",
"key-format": "infix-crypto-types:passphrase-key-format"
}
]
}
+4 -2
View File
@@ -107,8 +107,10 @@ To configure WiFi as a client, first store your WiFi password in the keystore:
```
admin@infix:/> configure
admin@infix:/config/> edit keystore symmetric-key mywifi
admin@infix:/config/keystore/…/mywifi/> set key-format wifi-preshared-key-format
admin@infix:/config/keystore/…/mywifi/> set symmetric-key YourWiFiPassword
admin@infix:/config/keystore/…/mywifi/> set key-format passphrase-key-format
admin@infix:/config/keystore/…/mywifi/> change cleartext-symmetric-key
Passphrase: ************
Retype passphrase: ************
admin@infix:/config/keystore/…/mywifi/> leave
```
+35 -9
View File
@@ -1,23 +1,49 @@
#!/bin/sh
# Prompt for a secret with confirmation, then encode and output it.
#
# Default mode: hash with mkpasswd (for system passwords)
# askpass [OUTPUT]
#
# Base64 mode (-b): base64-encode (for keystore passphrases)
# askpass -b [OUTPUT]
#
# If OUTPUT is given, result is written to the file.
# If omitted, result is written to stdout.
# shellcheck disable=SC3045
LABEL="New password"
MODE=hash
if [ "$1" = "-b" ]; then
LABEL="Passphrase"
MODE=base64
shift
fi
OUTPUT=$1
read -r -s -p "New password: " password
read -r -s -p "$LABEL: " secret
>&2 echo
read -r -s -p "Retype password: " password_again
read -r -s -p "Retype $LABEL: " secret_again
>&2 echo
if [ "$password" != "$password_again" ]; then
echo "Passwords do not match, try again."
if [ "$secret" != "$secret_again" ]; then
echo "${LABEL}s do not match, try again."
exit 1
fi
if [ -z "$OUTPUT" ]; then
echo "$password"
exit 0
if [ -z "$secret" ]; then
echo "Empty $LABEL, try again."
exit 1
fi
if [ "$MODE" = "base64" ]; then
encoded=$(printf '%s' "$secret" | base64 -w 0)
else
encoded=$(printf '%s\n' "$secret" | mkpasswd -s)
fi
umask 0177
echo "$password" | mkpasswd -s > "$OUTPUT"
exit 0
if [ -z "$OUTPUT" ]; then
echo "$encoded"
else
printf '%s' "$encoded" > "$OUTPUT"
fi
+5 -2
View File
@@ -3,7 +3,7 @@ Change Log
All notable changes to the project are documented in this file.
[v26.01.0][UNRELEASED]
[v26.01.0][] - 2026-02-03
-------------------------
> [!IMPORTANT]
@@ -23,7 +23,7 @@ All notable changes to the project are documented in this file.
Noteworthy changes and additions in this release are marked below in bold text.
- Upgrade Linux kernel to 6.18.8 (LTS)
- **Upgrade Linux kernel from 6.12.65 to 6.18.8 (LTS)**
- Upgrade Buildroot to 2025.02.10 (LTS)
- Upgrade libyang to 4.2.2
- Upgrade sysrepo to 4.2.10
@@ -53,6 +53,9 @@ Noteworthy changes and additions in this release are marked below in bold text.
removed during upgrade (for the rest of the configuration to apply) and you
need to reconfigure them again. See the [WiFi][] documentation for details
- Add support for **WireGuard VPN tunnels**.
- Updated CLI change command to support `cleartext-symmetric-key` (type binary).
Used by both WireGuard and WiFi, with application-specific `key-format` for
keys and passphrases
- New default NACM privilege levels (user levels) in `factory-config`:
`operator` (network & container manager) and `guest` (read-only). For
details, see the updated system configuration documentation, as well as a
+193
View File
@@ -0,0 +1,193 @@
# Keystore
The Infix keystore is a centralized storage system for cryptographic keys
used throughout the system. It is based on the IETF standards [RFC 9641][1]
(Keystore) and [RFC 9640][2] (Cryptographic Types), with Infix extensions
for WiFi and WireGuard key formats.
## Overview
The keystore supports two types of cryptographic keys:
1. **Asymmetric Keys** — public/private key pairs used for:
- SSH host authentication (RSA keys)
- WireGuard VPN tunnels (X25519 keys)
2. **Symmetric Keys** — shared secrets used for:
- WiFi authentication (WPA2/WPA3 pre-shared keys)
- WireGuard VPN pre-shared keys
All keys are stored under the `ietf-keystore` configuration path and can be
managed via CLI, NETCONF, or RESTCONF.
### Supported Formats
| **Asymmetric Key Format** | **Use Case** | **Key Type** |
|----------------------------------------------------------|---------------|--------------|
| `rsa-private-key-format` / `ssh-public-key-format` | SSH host keys | RSA |
| `x25519-private-key-format` / `x25519-public-key-format` | WireGuard VPN | Curve25519 |
| **Symmetric Key Format** | **Use Case** |
|-----------------------------|-----------------------------------|
| `passphrase-key-format` | Human-readable passphrases (WiFi) |
| `octet-string-key-format` | Raw symmetric keys (WireGuard) |
## Asymmetric Keys
Asymmetric keys consist of a public/private key pair. The public key can be
shared freely, while the private key must be kept secure.
### SSH Host Keys
SSH host keys identify the system during SSH and NETCONF connections. The
default host key is automatically generated on first boot and stored in the
keystore with the name `genkey`.
See [SSH Management](management.md) for details on generating and importing
custom SSH host keys.
### WireGuard Keys
WireGuard uses X25519 elliptic curve cryptography for key exchange. Each
WireGuard interface requires a public/private key pair stored as an asymmetric
key in the keystore. Key pairs can be generated directly from the CLI:
<pre class="cli"><code>admin@example:/> <b>wireguard genkey</b>
Private: aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=
Public: bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP=
</code></pre>
See [WireGuard VPN](vpn-wireguard.md) for key generation and configuration
examples.
## Symmetric Keys
Symmetric keys are shared secrets where the same key must be configured on
all systems that need to communicate.
### WiFi Pre-Shared Keys
WiFi networks secured with WPA2 or WPA3 use pre-shared keys stored as
symmetric keys in the keystore with `passphrase-key-format`. The
passphrase must be 8-63 printable ASCII characters.
Since symmetric keys are stored as binary (base64-encoded), the CLI
provides the `change` command to enter passphrases interactively:
<pre class="cli"><code>admin@example:/config/keystore/…/my-wifi-key/> <b>change cleartext-symmetric-key</b>
Passphrase: ************
Retype passphrase: ************
</code></pre>
See [WiFi](wifi.md) for complete configuration examples.
### WireGuard Pre-Shared Keys
WireGuard supports optional pre-shared keys (PSK) that add a layer of
symmetric encryption alongside Curve25519. PSKs use the standard IETF
`octet-string-key-format` (32 random bytes). This provides defense-in-depth
against future quantum computers that might break elliptic curve cryptography.
Note, however, that WireGuards authentication and initial key agreement
remain Curve25519-based, so PSKs only protect the session encryption,
not the handshake itself.
PSKs can be generated directly from the CLI:
<pre class="cli"><code>admin@example:/> <b>wireguard genpsk</b>
cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=
</code></pre>
See [WireGuard VPN](vpn-wireguard.md) for PSK generation and usage examples.
## Viewing Keys
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:
<pre class="cli"><code>admin@example:/> <b>show keystore</b>
────────────────────────────────────────────────────────────────────────
<span class="title">Symmetric Keys</span>
<span class="header">NAME FORMAT VALUE </span>
my-wifi-key passphrase MySecretPassword
wg-psk octet-string zYr83O4Ykj9i1gN+/aaosJxQx...
────────────────────────────────────────────────────────────────────────
<span class="title">Asymmetric Keys</span>
<span class="header">NAME TYPE PUBLIC KEY </span>
genkey rsa MIIBCgKCAQEAnj0YinjhYDgYbEGuh7...
wg-tunnel x25519 bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1...
</code></pre>
To see the full (untruncated) details of a specific key, use the
`symmetric` or `asymmetric` qualifier with the key name:
<pre class="cli"><code>admin@example:/> <b>show keystore symmetric my-wifi-key</b>
name : my-wifi-key
format : passphrase
value : MySecretPassword
admin@example:/> <b>show keystore asymmetric genkey</b>
name : genkey
algorithm : rsa
public key format : ssh-public-key
public key : MIIBCgKCAQEAnj0YinjhY...full key...IDAQAB
</code></pre>
> [!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`:
<pre class="cli"><code>admin@example:/config/> <b>show keystore</b>
</code></pre>
> [!WARNING]
> 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
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>delete keystore asymmetric-key mykey</b>
admin@example:/config/> <b>leave</b>
</code></pre>
> [!CAUTION]
> Deleting a key that is referenced by a service (SSH, WireGuard, WiFi) will
> cause that service to fail. Verify the key is not in use before deletion.
## Security Considerations
The keystore is protected by NACM (Network Access Control Model) rules.
Only users in the `admin` group can view or modify cryptographic keys.
See [NACM](nacm.md) for details on access control.
Private keys are stored in cleartext in the configuration database.
Configuration files and backups containing the keystore should be treated
as sensitive and protected accordingly.
### Key Validation
Symmetric key values are stored as binary (base64-encoded). The system
validates them based on their declared format:
- `passphrase-key-format`: Used by WiFi, must decode to 8-63 ASCII characters
- `octet-string-key-format`: Used by Wireguard, must decode to exactly 32 bytes (256 bits)
## References
- [RFC 9641 - A YANG Data Model for a Keystore][1]
- [RFC 9640 - YANG Data Types and Groupings for Cryptography][2]
- [WiFi Documentation](wifi.md)
- [WireGuard VPN Documentation](vpn-wireguard.md)
- [SSH Management](management.md)
- [NACM Access Control](nacm.md)
[1]: https://datatracker.ietf.org/doc/html/rfc9641
[2]: https://datatracker.ietf.org/doc/html/rfc9640
+5
View File
@@ -47,6 +47,11 @@ RSA for now, thus the private key must be
`ietf-crypto-types:rsa-private-key-format` and the public key
`ietf-crypto-types:ssh-public-key-format`
> [!TIP]
> For comprehensive information about the keystore, including key management,
> security considerations, and examples for different key types, see the
> [Keystore documentation](keystore.md).
### Use your own SSH hostkeys
Hostkeys can be generated with OpenSSL:
+17 -28
View File
@@ -26,31 +26,25 @@ Key features of WireGuard:
WireGuard uses public-key cryptography similar to SSH. Each WireGuard interface
requires a private key, and each peer is identified by its public key.
**Generate a WireGuard key pair using the `wg` command:**
```bash
admin@example:~$ wg genkey | tee privatekey | wg pubkey > publickey
admin@example:~$ cat privatekey
aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=
admin@example:~$ cat publickey
bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP=
```
This generates a private key, saves it to `privatekey`, derives the public key,
and saves it to `publickey`.
**Import the private key into the keystore:**
**Import the key pair into the keystore:**
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit keystore asymmetric-key wg-site-a</b>
admin@example:/config/keystore/asymmetric-key/wg-site-a/> <b>do wireguard genkey</b>
Private: aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=
Public: bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP=
admin@example:/config/keystore/asymmetric-key/wg-site-a/> <b>set public-key-format x25519-public-key-format</b>
admin@example:/config/keystore/asymmetric-key/wg-site-a/> <b>set private-key-format x25519-private-key-format</b>
admin@example:/config/keystore/asymmetric-key/wg-site-a/> <b>set public-key bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP=</b>
admin@example:/config/keystore/asymmetric-key/wg-site-a/> <b>set private-key aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=</b>
admin@example:/config/keystore/asymmetric-key/wg-site-a/> <b>set cleartext-private-key aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=</b>
admin@example:/config/keystore/asymmetric-key/wg-site-a/> <b>leave</b>
admin@example:/>
</code></pre>
> [!TIP]
> The `do` prefix allows running admin-exec commands from configure context.
> Use the base for `wireguard genkey` when in admin-exec context.
**Import peer public keys into the truststore:**
<pre class="cli"><code>admin@example:/> <b>configure</b>
@@ -63,8 +57,7 @@ admin@example:/>
> [!IMPORTANT]
> Keep private keys secure! Never share your private key. Only exchange
> public keys with peers. Delete the `privatekey` file after importing it
> into the keystore.
> public keys with peers.
## Point-to-Point Configuration
@@ -394,20 +387,17 @@ recording traffic today would still need the PSK even if they break Curve25519
later. However, peer authentication still relies on Curve25519, so PSKs don't
provide complete post-quantum security.
**Generate a preshared key using `wg genpsk`:**
**Generate a preshared key:**
```bash
admin@example:~$ wg genpsk > preshared.key
admin@example:~$ cat preshared.key
<pre class="cli"><code>admin@example:/config/> <b>do wireguard genpsk</b>
cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=
```
</code></pre>
**Import the preshared key into the keystore:**
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit keystore symmetric-key wg-psk</b>
admin@example:/config/keystore/symmetric-key/wg-psk/> <b>set key-format wireguard-symmetric-key-format</b>
admin@example:/config/keystore/symmetric-key/wg-psk/> <b>set key cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=</b>
<pre class="cli"><code>admin@example:/config/> <b>edit keystore symmetric-key wg-psk</b>
admin@example:/config/keystore/symmetric-key/wg-psk/> <b>set key-format octet-string-key-format</b>
admin@example:/config/keystore/symmetric-key/wg-psk/> <b>set cleartext-symmetric-key cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=</b>
admin@example:/config/keystore/symmetric-key/wg-psk/> <b>end</b>
admin@example:/config/interface/wg0/> <b>edit wireguard peers wg-peers peer remote</b>
admin@example:/config/interface/…/wg-peers/peer/remote/> <b>set preshared-key wg-psk</b>
@@ -420,5 +410,4 @@ on both sides.
> [!IMPORTANT]
> Preshared keys must be kept secret and exchanged through a secure channel,
> just like passwords. Delete the `preshared.key` file after importing it
> into both peer keystores.
> just like passwords.
+45 -14
View File
@@ -241,6 +241,23 @@ In the CLI, signal strength is reported as: excellent, good, fair or bad.
For precise signal strength values in dBm, use NETCONF or RESTCONF to access
the `signal-strength` leaf in the operational datastore.
## Passphrase Requirements
To ensure your connection is secure and compatible with all network
hardware, your passphrase must meet the following criteria:
- Length: Between 8 and 63 characters
- Characters: Use only standard English keyboard characters
- Allowed: Letters (A-Z, a-z), numbers (0-9), and common symbols (e.g., ! @ # $ % ^ & * ( ) _ + - = [ ] { } | ; : ' " , . < > / ? ~)
- Spaces: Spaces are allowed, but not at the very beginning or very end of the passphrase
- Prohibited: Emojis, accented characters (like á or ñ), and special "control" characters
> [!TIP] Why the limit?
> Standard WiFi security (WPA2/WPA3) requires a minimum of 8 characters to
> prevent "brute-force" hacking. The character limit ensures your password
> works on older routers and various operating systems.
> Tips for password strength, see [XKCD #936](https://xkcd.com/936/).
## Station Mode (Client)
Station mode connects to an existing Wi-Fi network. Before configuring station
@@ -253,11 +270,16 @@ Create a keystore entry for your WiFi password (8-63 characters):
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit keystore symmetric-key my-wifi-key</b>
admin@example:/config/keystore/…/my-wifi-key/> <b>set key-format wifi-preshared-key-format</b>
admin@example:/config/keystore/…/my-wifi-key/> <b>set symmetric-key MyPassword123</b>
admin@example:/config/keystore/…/my-wifi-key/> <b>set key-format passphrase-key-format</b>
admin@example:/config/keystore/…/my-wifi-key/> <b>change cleartext-symmetric-key</b>
Passphrase: ************
Retype passphrase: ************
admin@example:/config/keystore/…/my-wifi-key/> <b>leave</b>
</code></pre>
The `change` command prompts for the passphrase interactively and
handles the base64 encoding required by the keystore automatically.
### Step 2: Connect to Network
Configure station mode with the SSID and password to connect:
@@ -276,8 +298,9 @@ name : wifi0
type : wifi
operational status : up
physical address : f0:09:0d:36:5f:86
SSID : MyHomeNetwork
Signal : excellent
mode : station
ssid : MyHomeNetwork
signal : -52 dBm (good)
</code></pre>
**Station configuration parameters:**
@@ -310,8 +333,10 @@ create a keystore entry for your WiFi password and configure the AP interface:
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit keystore symmetric-key my-wifi-secret</b>
admin@example:/config/keystore/…/my-wifi-secret/> <b>set key-format wifi-preshared-key-format</b>
admin@example:/config/keystore/…/my-wifi-secret/> <b>set symmetric-key MySecurePassword123</b>
admin@example:/config/keystore/…/my-wifi-secret/> <b>set key-format passphrase-key-format</b>
admin@example:/config/keystore/…/my-wifi-secret/> <b>change cleartext-symmetric-key</b>
Passphrase: ************
Retype passphrase: ************
admin@example:/config/keystore/…/my-wifi-secret/> <b>end</b>
</code></pre>
@@ -371,14 +396,20 @@ admin@example:/config/hardware/component/radio0/wifi-radio/> <b>leave</b>
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit keystore symmetric-key main-secret</b>
admin@example:/config/keystore/…/main-secret/> <b>set key-format wifi-preshared-key-format</b>
admin@example:/config/keystore/…/main-secret/> <b>set symmetric-key MyMainPassword</b>
admin@example:/config/keystore/…/main-secret/> <b>set key-format passphrase-key-format</b>
admin@example:/config/keystore/…/main-secret/> <b>change cleartext-symmetric-key</b>
Passphrase: ************
Retype passphrase: ************
admin@example:/config/> <b>edit keystore symmetric-key guest-secret</b>
admin@example:/config/keystore/…/guest-secret/> <b>set key-format wifi-preshared-key-format</b>
admin@example:/config/keystore/…/guest-secret/> <b>set symmetric-key GuestPassword123</b>
admin@example:/config/keystore/…/guest-secret/> <b>set key-format passphrase-key-format</b>
admin@example:/config/keystore/…/guest-secret/> <b>change cleartext-symmetric-key</b>
Passphrase: ************
Retype passphrase: ************
admin@example:/config/> <b>edit keystore symmetric-key iot-secret</b>
admin@example:/config/keystore/…/iot-secret/> <b>set key-format wifi-preshared-key-format</b>
admin@example:/config/keystore/…/iot-secret/> <b>set symmetric-key IoTDevices2025</b>
admin@example:/config/keystore/…/iot-secret/> <b>set key-format passphrase-key-format</b>
admin@example:/config/keystore/…/iot-secret/> <b>change cleartext-symmetric-key</b>
Passphrase: ************
Retype passphrase: ************
admin@example:/config/keystore/…/iot-secret/> <b>leave</b>
</code></pre>
@@ -466,8 +497,8 @@ If issues arise, try the following troubleshooting steps:
1. **Verify signal strength**: Check that the target network shows
"good" or "excellent" signal in scan results
2. **Check credentials**: Verify the preshared key in the keystore
matches the network password
2. **Check credentials**: Use `show keystore symmetric <name>` to verify
the passphrase matches the network password
3. **Review logs**: Check system logs with `show log` for Wi-Fi related
errors
4. **Regulatory compliance**: Ensure the country-code on the radio
+1
View File
@@ -53,6 +53,7 @@ nav:
- Access Control (NACM): nacm.md
- Hardware Info & Status: hardware.md
- Management: management.md
- Keystore: keystore.md
- Syslog Support: syslog.md
- Support Data: support.md
- Upgrade: upgrade.md
@@ -1,3 +1,3 @@
# Locally calculated
sha256 9d9d33b873917ca5d0bdcc47a36d2fd385971ab0c045d1472fcadf95ee5bcf5b LICENCE
sha256 6e098390be2a78a56cf94eecb9d28b11c1791dd2c364e59602bbd664b508fd57 klish-plugin-sysrepo-a4b1fae697b51614dc75989e8f4fc8d277689d16-git4.tar.gz
sha256 7bfdaef838ee8bd3995140c40144abe79ff1c6391c7b48445e6728cd4e7e56a8 klish-plugin-sysrepo-2f14503c7ea6eb24c8adaf0cf2cf7a511114b09e-git4.tar.gz
@@ -4,7 +4,7 @@
#
################################################################################
KLISH_PLUGIN_SYSREPO_VERSION = a4b1fae697b51614dc75989e8f4fc8d277689d16
KLISH_PLUGIN_SYSREPO_VERSION = 2f14503c7ea6eb24c8adaf0cf2cf7a511114b09e
KLISH_PLUGIN_SYSREPO_SITE = https://github.com/kernelkit/klish-plugin-sysrepo.git
#KLISH_PLUGIN_SYSREPO_VERSION = cdd3eb51a7f7ee0ed5bd925fa636061d3b1b85fb
#KLISH_PLUGIN_SYSREPO_SITE = https://src.libcode.org/pkun/klish-plugin-sysrepo.git
+21 -1
View File
@@ -190,7 +190,7 @@ def interface(args: List[str]) -> None:
elif len(args) == 1:
iface = args[0]
if is_valid_interface_name(iface):
cli_pretty(data, f"show-interfaces", "-n", iface)
cli_pretty(data, "show-interfaces", "-n", iface)
else:
print(f"Invalid interface name: {iface}")
else:
@@ -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 <name> | asymmetric <name>]")
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,
@@ -1,5 +1,13 @@
#!/bin/sh
# Rename cleartext-key to symmetric-key
# Migrate symmetric keys from v1.6 (v25.11) to IETF standard format.
#
# v1.6 had WiFi keys stored as:
# - infix-keystore:cleartext-key (type string, plaintext)
# - infix-keystore:key-format wifi-preshared-key-format
#
# v1.7 uses the IETF standard leaf and updated format names:
# - cleartext-symmetric-key (type binary, base64-encoded)
# - key-format passphrase-key-format
file=$1
temp=${file}.tmp
@@ -7,12 +15,17 @@ temp=${file}.tmp
jq '
if .["ietf-keystore:keystore"]?."symmetric-keys"?."symmetric-key" then
.["ietf-keystore:keystore"]."symmetric-keys"."symmetric-key" |= map(
if ."infix-keystore:key-format" then
del(."infix-keystore:key-format") |
. + { "key-format": "infix-crypto-types:passphrase-key-format" }
else
.
end |
if ."infix-keystore:cleartext-key" then
# Rename cleartext-key to symmetric-key
."infix-keystore:cleartext-key" as $key_value |
del(."infix-keystore:cleartext-key") | . + {
"infix-keystore:symmetric-key": $key_value
}
."infix-keystore:cleartext-key" as $val |
del(."infix-keystore:cleartext-key") |
. + { "cleartext-symmetric-key": ($val | @base64) }
else
.
end
+13 -5
View File
@@ -12,6 +12,7 @@
#include "core.h"
#include "interfaces.h"
#include "dagger.h"
#include "base64.h"
#define XPATH_BASE_ "/ietf-hardware:hardware"
#define HOSTAPD_CONF "/etc/hostapd-%s.conf"
@@ -235,8 +236,9 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name,
/* Helper: Write SSID and security configuration (shared between primary and BSS) */
static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd_node *config, bool is_bss)
{
const char *ssid, *hidden, *security_mode, *secret_name, *secret;
const char *ssid, *hidden, *security_mode, *secret_name;
struct lyd_node *wifi, *ap, *security, *secret_node;
unsigned char *secret = NULL;
const char *ifname;
char bssid[18];
@@ -282,15 +284,19 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
security_mode = "open";
/* Get secret from keystore if needed */
secret = NULL;
if (strcmp(security_mode, "open") != 0) {
secret_name = lydx_get_cattr(security, "secret");
if (secret_name) {
const char *b64;
secret_node = lydx_get_xpathf(config,
"/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key",
"/keystore/symmetric-keys/symmetric-key[name='%s']/cleartext-symmetric-key",
secret_name);
if (secret_node)
secret = lyd_get_value(secret_node);
if (secret_node) {
b64 = lyd_get_value(secret_node);
if (b64)
secret = base64_decode((const unsigned char *)b64, strlen(b64), NULL);
}
}
}
@@ -329,6 +335,8 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
/* ieee80211w=1: MFP capable but optional, for WPA2 client compatibility */
fprintf(hostapd, "ieee80211w=1\n");
}
free(secret);
}
/* Helper: Write radio-specific configuration */
+76 -8
View File
@@ -7,14 +7,76 @@
* configuration (hostapd) is handled by hardware.c.
*/
#include <ctype.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>
#include "interfaces.h"
#include "base64.h"
#define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf"
int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif)
{
struct lyd_node *wifi, *station, *security, *secret_node;
const char *ifname, *secret_name, *security_mode, *b64;
unsigned char *decoded;
size_t len;
ifname = lydx_get_cattr(cif, "name");
wifi = lydx_get_child(cif, "wifi");
if (!wifi)
return SR_ERR_OK;
station = lydx_get_child(wifi, "station");
if (!station)
return SR_ERR_OK;
security = lydx_get_child(station, "security");
security_mode = lydx_get_cattr(security, "mode");
secret_name = lydx_get_cattr(security, "secret");
if (!secret_name || !strcmp(security_mode, "disabled"))
return SR_ERR_OK;
secret_node = lydx_get_xpathf(cif,
"../../keystore/symmetric-keys/symmetric-key[name='%s']",
secret_name);
b64 = lydx_get_cattr(secret_node, "cleartext-symmetric-key");
if (!b64 || !*b64)
return SR_ERR_OK;
decoded = base64_decode((const unsigned char *)b64, strlen(b64), &len);
if (!decoded)
return SR_ERR_OK;
if (len < 8 || len > 63) {
if (session)
sr_session_set_error_message(session,
"%s: WiFi passphrase must be 8-63 characters, got %zu",
ifname, len);
free(decoded);
return SR_ERR_VALIDATION_FAILED;
}
for (size_t i = 0; i < len; i++) {
if (!isprint((unsigned char)decoded[i])) {
if (session)
sr_session_set_error_message(session,
"%s: WiFi passphrase contains non-printable "
"character at position %zu",
ifname, i + 1);
free(decoded);
return SR_ERR_VALIDATION_FAILED;
}
}
free(decoded);
return SR_ERR_OK;
}
wifi_mode_t wifi_get_mode(struct lyd_node *iface)
{
struct lyd_node *ap, *wifi;
@@ -59,8 +121,9 @@ int wifi_mode_changed(struct lyd_node *wifi)
*/
int wifi_gen_station(struct lyd_node *cif)
{
const char *ifname, *ssid, *secret_name, *secret, *security_mode, *radio;
const char *ifname, *ssid, *secret_name, *security_mode, *radio;
struct lyd_node *security, *secret_node, *radio_node, *station, *wifi;
unsigned char *secret = NULL;
FILE *wpa_supplicant = NULL;
char *security_str = NULL;
const char *country;
@@ -91,12 +154,14 @@ int wifi_gen_station(struct lyd_node *cif)
country = lydx_get_cattr(radio_node, "country-code");
if (secret_name && strcmp(security_mode, "disabled") != 0) {
const char *b64;
secret_node = lydx_get_xpathf(cif,
"../../keystore/symmetric-keys/symmetric-key[name='%s']",
secret_name);
secret = lydx_get_cattr(secret_node, "symmetric-key");
} else {
secret = NULL;
b64 = lydx_get_cattr(secret_node, "cleartext-symmetric-key");
if (b64)
secret = base64_decode((const unsigned char *)b64, strlen(b64), NULL);
}
oldmask = umask(0077);
@@ -121,11 +186,13 @@ int wifi_gen_station(struct lyd_node *cif)
/* If SSID is present, create network block. Otherwise, scan-only mode */
if (ssid) {
/* Station mode with network configured */
if (!strcmp(security_mode, "disabled")) {
if (!strcmp(security_mode, "disabled"))
asprintf(&security_str, "key_mgmt=NONE");
} else if (secret) {
asprintf(&security_str, "key_mgmt=SAE WPA-PSK\npsk=\"%s\"", secret);
}
else if (secret)
asprintf(&security_str,
"key_mgmt=SAE WPA-PSK\n"
" psk=\"%s\"", secret);
fprintf(wpa_supplicant,
"network={\n"
" bgscan=\"simple: 30:-45:300\"\n"
@@ -152,6 +219,7 @@ int wifi_gen_station(struct lyd_node *cif)
}
out:
free(secret);
if (wpa_supplicant)
fclose(wpa_supplicant);
umask(oldmask);
+72
View File
@@ -2,9 +2,81 @@
#include <srx/lyx.h>
#include "interfaces.h"
#include "base64.h"
#define WIREGUARD_CONFIG "/run/wireguard-%s.conf"
static int wireguard_validate_psk(sr_session_ctx_t *session, struct lyd_node *cif,
const char *ifname, const char *psk_ref)
{
struct lyd_node *psk_node;
const char *psk_data;
unsigned char *decoded;
size_t len;
psk_node = lydx_get_xpathf(cif,
"../../keystore/symmetric-keys/symmetric-key[name='%s']",
psk_ref);
if (!psk_node)
return SR_ERR_OK;
psk_data = lydx_get_cattr(psk_node, "cleartext-symmetric-key");
if (!psk_data || !*psk_data)
return SR_ERR_OK;
decoded = base64_decode((const unsigned char *)psk_data, strlen(psk_data), &len);
if (!decoded)
return SR_ERR_OK;
if (len != 32) {
if (session)
sr_session_set_error_message(session,
"%s: WireGuard preshared key '%s' must be "
"exactly 32 bytes, got %zu",
ifname, psk_ref, len);
free(decoded);
return SR_ERR_VALIDATION_FAILED;
}
free(decoded);
return SR_ERR_OK;
}
int wireguard_validate_peers(sr_session_ctx_t *session, struct lyd_node *cif)
{
const char *ifname = lydx_get_cattr(cif, "name");
struct lyd_node *wg, *bag_peer, *peer;
wg = lydx_get_child(cif, "wireguard");
if (!wg)
return SR_ERR_OK;
LYX_LIST_FOR_EACH(lyd_child(wg), bag_peer, "peers") {
const char *psk_ref;
int rc;
/* Validate bag-level PSK */
psk_ref = lydx_get_cattr(bag_peer, "preshared-key");
if (psk_ref) {
rc = wireguard_validate_psk(session, cif, ifname, psk_ref);
if (rc)
return rc;
}
/* Validate per-peer PSK overrides */
LYX_LIST_FOR_EACH(lyd_child(bag_peer), peer, "peer") {
psk_ref = lydx_get_cattr(peer, "preshared-key");
if (psk_ref) {
rc = wireguard_validate_psk(session, cif, ifname, psk_ref);
if (rc)
return rc;
}
}
}
return SR_ERR_OK;
}
/* Helper to get a peer setting with override logic:
* 1. Check peer-specific override
* 2. Fall back to key-bag level default
+39 -3
View File
@@ -391,9 +391,11 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net,
case IFT_VXLAN:
return vxlan_gen(NULL, cif, ip);
case IFT_WIFI:
return wifi_add_iface(cif, net);
return wifi_validate_secret(session, cif)
? : wifi_add_iface(cif, net);
case IFT_WIREGUARD:
return wireguard_gen(NULL, cif, ip, net);
return wireguard_validate_peers(session, cif)
? : wireguard_gen(NULL, cif, ip, net);
case IFT_ETH:
return netdag_gen_ethtool(net, cif, dif);
case IFT_LO:
@@ -424,7 +426,8 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
return netdag_gen_ethtool(net, cif, dif);
case IFT_WIFI:
if (wifi_get_mode(cif) == wifi_station)
return wifi_gen_station(cif);
return wifi_validate_secret(session, cif)
? : wifi_gen_station(cif);
return 0;
case IFT_DUMMY:
case IFT_GRE:
@@ -819,6 +822,39 @@ err_out:
return err;
}
int interfaces_validate_keys(sr_session_ctx_t *session, struct lyd_node *config)
{
struct lyd_node *ifaces, *iface;
int rc;
ifaces = lydx_get_descendant(config, "interfaces", "interface", NULL);
LYX_LIST_FOR_EACH(ifaces, iface, "interface") {
const char *ifname = lydx_get_cattr(iface, "name");
switch (iftype_from_iface(iface)) {
case IFT_WIFI:
rc = wifi_validate_secret(session, iface);
break;
case IFT_WIREGUARD:
rc = wireguard_validate_peers(session, iface);
break;
default:
rc = SR_ERR_OK;
break;
}
if (!rc)
continue;
if (session)
return rc;
ERROR("%s: key fails validation, check keystore", ifname);
}
return SR_ERR_OK;
}
int interfaces_get_all_l3(const struct lyd_node *tree, char ***ifaces)
{
struct lyd_node *interfaces, *cif;
+3
View File
@@ -105,6 +105,7 @@ const char *get_chassis_addr(void);
int interface_get_phys_addr(struct lyd_node *cif, char *mac);
int link_gen_address(struct lyd_node *cif, FILE *ip);
int interfaces_get_all_l3(const struct lyd_node *tree, char ***ifaces);
int interfaces_validate_keys(sr_session_ctx_t *session, struct lyd_node *config);
/* ip.c */
int netdag_gen_ipv6_autoconf(struct dagger *net, struct lyd_node *cif,
@@ -130,6 +131,7 @@ typedef enum wifi_mode_t {
wifi_unknown
} wifi_mode_t;
int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif);
int wifi_add_iface(struct lyd_node *cif, struct dagger *net);
int wifi_del_iface(struct lyd_node *dif, struct dagger *net);
int wifi_mode_changed(struct lyd_node *wifi);
@@ -162,6 +164,7 @@ int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path);
int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
/* infix-if-wireguard */
int wireguard_validate_peers(sr_session_ctx_t *session, struct lyd_node *cif);
int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net);
#endif /* CONFD_INTERFACES_H_ */
+10 -2
View File
@@ -8,8 +8,10 @@
#include "base64.h"
#include "core.h"
#include "interfaces.h"
#define XPATH_KEYSTORE_ "/ietf-keystore:keystore/asymmetric-keys"
#define XPATH_KEYSTORE_ASYM "/ietf-keystore:keystore/asymmetric-keys"
#define XPATH_KEYSTORE_SYM "/ietf-keystore:keystore/symmetric-keys"
#define SSH_PRIVATE_KEY "/tmp/ssh.key"
#define SSH_PUBLIC_KEY "/tmp/ssh.pub"
@@ -166,7 +168,8 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
struct lyd_node *changes, *change;
int rc = SR_ERR_OK;
if (diff && !lydx_find_xpathf(diff, XPATH_KEYSTORE_))
if (diff && !lydx_find_xpathf(diff, XPATH_KEYSTORE_ASYM)
&& !lydx_find_xpathf(diff, XPATH_KEYSTORE_SYM))
return SR_ERR_OK;
switch (event) {
@@ -174,6 +177,9 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
rc = keystore_update(session, config, diff);
break;
case SR_EV_CHANGE:
if (diff && lydx_find_xpathf(diff, XPATH_KEYSTORE_SYM))
rc = interfaces_validate_keys(session, config);
break;
case SR_EV_ENABLED:
break;
case SR_EV_ABORT:
@@ -186,6 +192,8 @@ int keystore_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
if (rename(SSH_HOSTKEYS_NEXT, SSH_HOSTKEYS))
ERRNO("Failed switching to new %s", SSH_HOSTKEYS);
}
if (diff && lydx_find_xpathf(diff, XPATH_KEYSTORE_SYM))
interfaces_validate_keys(NULL, config);
return SR_ERR_OK;
default:
return SR_ERR_OK;
+21 -16
View File
@@ -15,32 +15,46 @@ module infix-crypto-types {
revision 2025-02-04 {
description "Initial";
}
identity private-key-format {
base ct:private-key-format;
description
"Base key-format identity for private keys.";
"Base for Infix private key format extensions.";
}
identity public-key-format {
base ct:public-key-format;
description
"Base key-format identity for public keys.";
"Base for Infix public key format extensions.";
}
identity rsa-private-key-format {
base private-key-format;
base ct:rsa-private-key-format;
description
"RSA private key in PKCS#1 format (RFC 8017).
Used for SSH host keys.";
}
identity ssh-public-key-format {
base public-key-format;
base ct:ssh-public-key-format;
description
"SSH public key format (RFC 4253, Section 6.6).
Used for SSH host keys.";
}
identity symmetric-key-format {
base ct:symmetric-key-format;
description
"Base for symmetric key format";
"Base for Infix symmetric key format extensions.";
}
identity wifi-preshared-key-format {
identity passphrase-key-format {
base ct:symmetric-key-format;
base symmetric-key-format;
description
"WiFi secret key";
"Human-readable passphrase, e.g., WiFi WPA2/WPA3 passwords.";
}
identity x25519-public-key-format {
@@ -48,7 +62,7 @@ module infix-crypto-types {
base ct:public-key-format;
description
"X25519 (Curve25519) public key format for Diffie-Hellman key exchange.
This is the format used by WireGuard.";
This format is used by network protocols such as WireGuard VPN.";
}
identity x25519-private-key-format {
@@ -56,15 +70,6 @@ module infix-crypto-types {
base ct:private-key-format;
description
"X25519 (Curve25519) private key format for Diffie-Hellman key exchange.
This is the format used by WireGuard.";
}
identity wireguard-symmetric-key-format {
base ct:symmetric-key-format;
base symmetric-key-format;
description
"WireGuard pre-shared key format.
32-byte base64-encoded key used as an optional additional layer
of symmetric encryption for post-quantum resistance.";
This format is used by network protocols such as WireGuard VPN.";
}
}
+5 -2
View File
@@ -10,6 +10,9 @@ submodule infix-if-wireguard {
import ietf-inet-types {
prefix inet;
}
import ietf-crypto-types {
prefix ct;
}
import infix-crypto-types {
prefix ixct;
}
@@ -75,8 +78,8 @@ submodule infix-if-wireguard {
This provides post-quantum resistance as an attacker would need
to break both the Curve25519 key exchange and this symmetric key.";
must "derived-from-or-self(deref(.)/../infix-ks:key-format, 'ixct:wireguard-symmetric-key-format')" {
error-message "Preshared key must be in wireguard-symmetric-key-format";
must "derived-from-or-self(deref(.)/../ks:key-format, 'ct:octet-string-key-format')" {
error-message "Preshared key must be in octet-string-key-format";
}
}
+1 -59
View File
@@ -5,15 +5,12 @@ module infix-keystore {
import ietf-keystore {
prefix ks;
}
import ietf-crypto-types {
prefix ct;
}
import infix-crypto-types {
prefix infix-ct;
}
revision 2025-12-17 {
description "Add WireGuard support";
description "Add WireGuard support, see infix-crypto-types.yang";
}
revision 2025-12-10 {
description "Adapt to changes in final version of ietf-keystore";
@@ -24,59 +21,4 @@ module infix-keystore {
revision 2025-02-04 {
description "Initial";
}
deviation "/ks:keystore/ks:asymmetric-keys/ks:asymmetric-key/ks:public-key-format" {
deviate replace {
type identityref {
base infix-ct:public-key-format;
}
}
}
deviation "/ks:keystore/ks:asymmetric-keys/ks:asymmetric-key/ks:private-key-format" {
deviate replace {
type identityref {
base infix-ct:private-key-format;
}
}
}
deviation "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-format" {
deviate not-supported;
}
augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key" {
leaf key-format {
type identityref {
base infix-ct:symmetric-key-format;
}
description
"Identifies the symmetric key's format
Valid symmetric key formats are:
wifi-preshared-key-format - WiFi preshared key
wireguard-symmetric-key-format - WireGuard preshared key";
}
}
deviation "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type/ks:cleartext-symmetric-key" {
deviate not-supported;
}
augment "/ks:keystore/ks:symmetric-keys/ks:symmetric-key/ks:key-type" {
case cleartext-symmetric-key {
leaf symmetric-key {
type string;
must "../infix-ks:key-format != 'infix-ct:wifi-preshared-key-format' or " +
"(string-length(.) >= 8 and string-length(.) <= 63)" {
error-message "WiFi pre-shared key must be 8-63 characters long";
}
must "../infix-ks:key-format != 'infix-ct:wireguard-symmetric-key-format' or " +
"string-length(.) = 44" {
error-message "WireGuard pre-shared key must be 44 characters (32-byte base64-encoded)";
}
description
"Cleartext symmetric key value.
Format depends on key-format:
- WiFi pre-shared key: 8-63 printable ASCII characters
- WireGuard pre-shared key: 32-byte base64-encoded key (44 chars with padding)";
}
}
}
}
+16
View File
@@ -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));
+43
View File
@@ -155,6 +155,20 @@
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="SYM_KEYS">
<COMPL>
<ACTION sym="sym_keys@infix"/>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="ASYM_KEYS">
<COMPL>
<ACTION sym="asym_keys@infix"/>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<VIEW name="main">
<HOTKEY key="^D" cmd="exit"/>
@@ -259,6 +273,21 @@
</COMMAND>
</COMMAND>
<COMMAND name="wireguard" help="WireGuard VPN key tools" mode="switch">
<COMMAND name="genkey" help="Generate a WireGuard private/public key pair">
<ACTION sym="script">
priv=$(wg genkey)
pub=$(printf '%s' "$priv" | wg pubkey)
echo "Private: $priv"
echo "Public: $pub"
</ACTION>
</COMMAND>
<COMMAND name="genpsk" help="Generate a WireGuard pre-shared key">
<ACTION sym="script">wg genpsk</ACTION>
</COMMAND>
</COMMAND>
<COMMAND name="set" help="Set operations, e.g., current date/time" mode="switch">
<COMMAND name="datetime" help="Set current date and time, ISO-8601 format">
<PARAM name="current-datetime" ptype="/STRING" help="yyyy-mm-ddThh:mm:ss(Z|+/-hh:mm)"/>
@@ -338,6 +367,20 @@
</ACTION>
</COMMAND>
<COMMAND name="keystore" help="Show keystore keys">
<SWITCH name="subcommands" min="0">
<COMMAND name="symmetric" help="Show details for a symmetric key">
<PARAM name="name" ptype="/SYM_KEYS" help="Key name"/>
<ACTION sym="script" in="tty" out="tty" interrupt="true">show keystore symmetric $KLISH_PARAM_name</ACTION>
</COMMAND>
<COMMAND name="asymmetric" help="Show details for an asymmetric key">
<PARAM name="name" ptype="/ASYM_KEYS" help="Key name"/>
<ACTION sym="script" in="tty" out="tty" interrupt="true">show keystore asymmetric $KLISH_PARAM_name</ACTION>
</COMMAND>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">show keystore</ACTION>
</COMMAND>
<COMMAND name="nacm" help="Show users and NACM status and groups">
<SWITCH name="subcommands" min="0">
<COMMAND name="group" help="Show details for a specific NACM group">
+133
View File
@@ -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 <name>")
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":
@@ -75,12 +75,12 @@ def configure_server(dut):
"symmetric-keys": {
"symmetric-key": [{
"name": "psk-client1",
"infix-keystore:symmetric-key": psk_client1,
"infix-keystore:key-format": "infix-crypto-types:wireguard-symmetric-key-format"
"cleartext-symmetric-key": psk_client1,
"key-format": "ietf-crypto-types:octet-string-key-format"
}, {
"name": "psk-client2",
"infix-keystore:symmetric-key": psk_client2,
"infix-keystore:key-format": "infix-crypto-types:wireguard-symmetric-key-format"
"cleartext-symmetric-key": psk_client2,
"key-format": "ietf-crypto-types:octet-string-key-format"
}]
}
}
@@ -226,8 +226,8 @@ def configure_client1(dut):
"symmetric-keys": {
"symmetric-key": [{
"name": "psk-server",
"infix-keystore:symmetric-key": psk_client1,
"infix-keystore:key-format": "infix-crypto-types:wireguard-symmetric-key-format"
"cleartext-symmetric-key": psk_client1,
"key-format": "ietf-crypto-types:octet-string-key-format"
}]
}
}
@@ -360,8 +360,8 @@ def configure_client2(dut):
"symmetric-keys": {
"symmetric-key": [{
"name": "psk-server",
"infix-keystore:symmetric-key": psk_client2,
"infix-keystore:key-format": "infix-crypto-types:wireguard-symmetric-key-format"
"cleartext-symmetric-key": psk_client2,
"key-format": "ietf-crypto-types:octet-string-key-format"
}]
}
}
+9
View File
@@ -22,6 +22,12 @@ may be supplied in places where a hostname is otherwise expected.
Commands:
scan [-a] [<iface>]
Discover Infix devices on the LAN using mDNS-SD (avahi-browse).
Use -a to show all mDNS devices, not just Infix. Falls back to
IPv6 link-local multicast ping if avahi-browse is not installed
and <iface> is specified.
peer <iface>
Return the address of the first IPv6 neighbor to respond on
<iface>'s local LAN.
@@ -75,6 +81,9 @@ case "$cmd" in
help)
usage && exit 0
;;
scan)
llscan "$@"
;;
peer)
llpeer "$@"
;;
+152
View File
@@ -144,3 +144,155 @@ llscp()
$sshpasscmd scp $LLSCP_OPTS "$src" "$dst"
}
# Usage: llscan [-a] [<iface>]
#
# Discover Infix devices (or all mDNS devices with -a) on the LAN.
# Prefers mDNS-SD via avahi-browse; falls back to IPv6 link-local
# multicast ping if avahi-browse is not available and an interface
# is specified.
llscan()
{
local all=0
local iface=
while [ $# -gt 0 ]; do
case "$1" in
-a)
all=1
shift
;;
*)
iface="$1"
shift
;;
esac
done
if [ "$iface" ] && [ ! -d "/sys/class/net/$iface" ]; then
echo "Error: Interface \"$iface\" does not exist." >&2
return 1
fi
if command -v avahi-browse >/dev/null 2>&1; then
llscan_mdns "$all"
elif [ "$iface" ]; then
echo "Note: avahi-browse not found, falling back to link-local scan." >&2
llscan_ll "$iface"
else
cat >&2 <<-EOF
Error: avahi-browse not found.
Install avahi-utils to scan for Infix devices via mDNS:
sudo apt install avahi-utils
Or specify an interface to use IPv6 link-local fallback:
$(basename $0) scan <iface>
EOF
return 1
fi
}
llscan_mdns()
{
local all="$1"
local flags="-tarp"
if avahi-browse --help 2>&1 | grep -q -- '-k'; then
flags="-tarpk"
fi
avahi-browse $flags | awk -F';' -v show_all="$all" '
$1 == "=" {
host = $7
proto = $3
addr = $8
txt = $10
on = ""; ov = ""; product = ""; serial = ""; devid = ""
n = split(txt, parts, "\" \"")
for (i = 1; i <= n; i++) {
gsub(/"/, "", parts[i])
if (parts[i] ~ /^on=/) { split(parts[i], kv, "="); on = kv[2] }
else if (parts[i] ~ /^ov=/) { split(parts[i], kv, "="); ov = kv[2] }
else if (parts[i] ~ /^product=/) { split(parts[i], kv, "="); product = kv[2] }
else if (parts[i] ~ /^serial=/) { split(parts[i], kv, "="); serial = kv[2] }
else if (parts[i] ~ /^deviceid=/) { split(parts[i], kv, "="); devid = kv[2] }
}
if (!show_all && on != "Infix") next
# Use deviceid (MAC) as unique key; fall back to hostname
key = devid ? devid : host
if (!product) product = on ? on : "-"
if (!ov) ov = "-"
if (!serial || serial == "null") serial = "-"
# Prefer IPv4 for display address
if (proto == "IPv4") {
ipv4[key] = addr
} else if (proto == "IPv6" && !ipv6[key]) {
ipv6[key] = addr
}
if (!seen[key]++) {
keys[++ndevs] = key
hosts[key] = host
products[key] = product
versions[key] = ov
serials[key] = serial
} else if (length(host) > length(hosts[key])) {
# Prefer the unique hostname (e.g., infix-c0-ff-ee.local)
# over the generic one (e.g., infix.local)
hosts[key] = host
}
}
END {
if (ndevs == 0) {
if (show_all)
print "No mDNS devices found." | "cat >&2"
else
print "No Infix devices found. Use -a to show all mDNS devices." | "cat >&2"
exit 1
}
fmt = "%-26s %-18s %-24s %-22s %s\n"
hdr = sprintf(fmt, "HOSTNAME", "ADDRESS", "PRODUCT", "VERSION", "SERIAL")
sub(/\n$/, "", hdr)
printf "\033[7m%s\033[0m\n", hdr
for (i = 1; i <= ndevs; i++) {
k = keys[i]
a = (ipv4[k] ? ipv4[k] : (ipv6[k] ? ipv6[k] : "-"))
p = products[k]; if (length(p) > 23) p = substr(p, 1, 22) "~"
v = versions[k]; if (length(v) > 21) v = substr(v, 1, 20) "~"
printf fmt, hosts[k], a, p, v, serials[k]
}
printf "\n%d device(s) found.\n", ndevs
}
'
}
llscan_ll()
{
local iface="$1"
printf "Scanning %s for IPv6 link-local neighbors ...\n\n" "$iface"
printf "\033[7m%-6s %s\033[0m\n" "#" "ADDRESS"
llping "$iface" -c3 -w3 2>/dev/null | awk '
/bytes from/ {
sub(/:$/, "", $4)
addr = $4
if (!seen[addr]++) {
printf "%-6d %s\n", ++n, addr
}
}
END {
printf "\n%d neighbor(s) found on '"$iface"'.\n", n+0
}
'
}