diff --git a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg index 672a768a5..723b25a28 100644 --- a/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg +++ b/board/aarch64/bananapi-bpi-r3/rootfs/usr/share/product/bananapi,bpi-r3/etc/factory-config.cfg @@ -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" } ] } diff --git a/board/aarch64/raspberrypi-rpi64/README.md b/board/aarch64/raspberrypi-rpi64/README.md index 5c9dff8cb..4619d12ca 100644 --- a/board/aarch64/raspberrypi-rpi64/README.md +++ b/board/aarch64/raspberrypi-rpi64/README.md @@ -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 ``` diff --git a/board/common/rootfs/usr/bin/askpass b/board/common/rootfs/usr/bin/askpass index c2896e889..7c47f1d21 100755 --- a/board/common/rootfs/usr/bin/askpass +++ b/board/common/rootfs/usr/bin/askpass @@ -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 diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 7486a2035..fdb8e52c5 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -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 diff --git a/doc/keystore.md b/doc/keystore.md new file mode 100644 index 000000000..55da5961c --- /dev/null +++ b/doc/keystore.md @@ -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: + +
admin@example:/> wireguard genkey
+Private: aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=
+Public:  bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP=
+
+ +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: + +
admin@example:/config/keystore/…/my-wifi-key/> change cleartext-symmetric-key
+Passphrase: ************
+Retype passphrase: ************
+
+ +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 WireGuard’s 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: + +
admin@example:/> wireguard genpsk
+cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=
+
+ +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: + +
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 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 + +
admin@example:/> configure
+admin@example:/config/> delete keystore asymmetric-key mykey
+admin@example:/config/> leave
+
+ +> [!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 diff --git a/doc/management.md b/doc/management.md index ece5abca4..1c3cb094c 100644 --- a/doc/management.md +++ b/doc/management.md @@ -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: diff --git a/doc/vpn-wireguard.md b/doc/vpn-wireguard.md index a5ceff8db..10255847e 100644 --- a/doc/vpn-wireguard.md +++ b/doc/vpn-wireguard.md @@ -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:**
admin@example:/> configure
 admin@example:/config/> edit keystore asymmetric-key wg-site-a
+admin@example:/config/keystore/asymmetric-key/wg-site-a/> do wireguard genkey
+Private: aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=
+Public:  bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP=
 admin@example:/config/keystore/asymmetric-key/wg-site-a/> set public-key-format x25519-public-key-format
 admin@example:/config/keystore/asymmetric-key/wg-site-a/> set private-key-format x25519-private-key-format
 admin@example:/config/keystore/asymmetric-key/wg-site-a/> set public-key bN1CwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP6KsrCwZ1lTP=
-admin@example:/config/keystore/asymmetric-key/wg-site-a/> set private-key aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=
+admin@example:/config/keystore/asymmetric-key/wg-site-a/> set cleartext-private-key aMqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP5JrqBvZqkSP=
 admin@example:/config/keystore/asymmetric-key/wg-site-a/> leave
 admin@example:/>
 
+> [!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:**
admin@example:/> configure
@@ -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
+
admin@example:/config/> do wireguard genpsk
 cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=
-```
+
**Import the preshared key into the keystore:** -
admin@example:/> configure
-admin@example:/config/> edit keystore symmetric-key wg-psk
-admin@example:/config/keystore/symmetric-key/wg-psk/> set key-format wireguard-symmetric-key-format
-admin@example:/config/keystore/symmetric-key/wg-psk/> set key cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=
+
admin@example:/config/> edit keystore symmetric-key wg-psk
+admin@example:/config/keystore/symmetric-key/wg-psk/> set key-format octet-string-key-format
+admin@example:/config/keystore/symmetric-key/wg-psk/> set cleartext-symmetric-key cO2DxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2mUQ7LtsrDxZ2m=
 admin@example:/config/keystore/symmetric-key/wg-psk/> end
 admin@example:/config/interface/wg0/> edit wireguard peers wg-peers peer remote
 admin@example:/config/interface/…/wg-peers/peer/remote/> set preshared-key wg-psk
@@ -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.
diff --git a/doc/wifi.md b/doc/wifi.md
index 73190f3fd..96183a6db 100644
--- a/doc/wifi.md
+++ b/doc/wifi.md
@@ -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):
 
 
admin@example:/> configure
 admin@example:/config/> edit keystore symmetric-key my-wifi-key
-admin@example:/config/keystore/…/my-wifi-key/> set key-format wifi-preshared-key-format
-admin@example:/config/keystore/…/my-wifi-key/> set symmetric-key MyPassword123
+admin@example:/config/keystore/…/my-wifi-key/> set key-format passphrase-key-format
+admin@example:/config/keystore/…/my-wifi-key/> change cleartext-symmetric-key
+Passphrase: ************
+Retype passphrase: ************
 admin@example:/config/keystore/…/my-wifi-key/> leave
 
+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)
**Station configuration parameters:** @@ -310,8 +333,10 @@ create a keystore entry for your WiFi password and configure the AP interface:
admin@example:/> configure
 admin@example:/config/> edit keystore symmetric-key my-wifi-secret
-admin@example:/config/keystore/…/my-wifi-secret/> set key-format wifi-preshared-key-format
-admin@example:/config/keystore/…/my-wifi-secret/> set symmetric-key MySecurePassword123
+admin@example:/config/keystore/…/my-wifi-secret/> set key-format passphrase-key-format
+admin@example:/config/keystore/…/my-wifi-secret/> change cleartext-symmetric-key
+Passphrase: ************
+Retype passphrase: ************
 admin@example:/config/keystore/…/my-wifi-secret/> end
 
@@ -371,14 +396,20 @@ admin@example:/config/hardware/component/radio0/wifi-radio/> leave
admin@example:/> configure
 admin@example:/config/> edit keystore symmetric-key main-secret
-admin@example:/config/keystore/…/main-secret/> set key-format wifi-preshared-key-format
-admin@example:/config/keystore/…/main-secret/> set symmetric-key MyMainPassword
+admin@example:/config/keystore/…/main-secret/> set key-format passphrase-key-format
+admin@example:/config/keystore/…/main-secret/> change cleartext-symmetric-key
+Passphrase: ************
+Retype passphrase: ************
 admin@example:/config/> edit keystore symmetric-key guest-secret
-admin@example:/config/keystore/…/guest-secret/> set key-format wifi-preshared-key-format
-admin@example:/config/keystore/…/guest-secret/> set symmetric-key GuestPassword123
+admin@example:/config/keystore/…/guest-secret/> set key-format passphrase-key-format
+admin@example:/config/keystore/…/guest-secret/> change cleartext-symmetric-key
+Passphrase: ************
+Retype passphrase: ************
 admin@example:/config/> edit keystore symmetric-key iot-secret
-admin@example:/config/keystore/…/iot-secret/> set key-format wifi-preshared-key-format
-admin@example:/config/keystore/…/iot-secret/> set symmetric-key IoTDevices2025
+admin@example:/config/keystore/…/iot-secret/> set key-format passphrase-key-format
+admin@example:/config/keystore/…/iot-secret/> change cleartext-symmetric-key
+Passphrase: ************
+Retype passphrase: ************
 admin@example:/config/keystore/…/iot-secret/> leave
 
@@ -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 ` 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 diff --git a/mkdocs.yml b/mkdocs.yml index cc03ed1ef..73768a6e0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash index c38d7e9ca..c652e519d 100644 --- a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash +++ b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.hash @@ -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 diff --git a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk index 0c6b1daa7..488407fff 100644 --- a/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk +++ b/package/klish-plugin-sysrepo/klish-plugin-sysrepo.mk @@ -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 diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py index 9bccfac18..f86a164aa 100755 --- a/src/bin/show/__init__.py +++ b/src/bin/show/__init__.py @@ -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 | 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/confd/share/migrate/1.7/20-keystore-cleartext-key-rename.sh b/src/confd/share/migrate/1.7/20-keystore-cleartext-key-rename.sh index b55b42d79..067db0c62 100755 --- a/src/confd/share/migrate/1.7/20-keystore-cleartext-key-rename.sh +++ b/src/confd/share/migrate/1.7/20-keystore-cleartext-key-rename.sh @@ -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 diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c index 0ea34f7b4..6d441fe7f 100644 --- a/src/confd/src/hardware.c +++ b/src/confd/src/hardware.c @@ -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 */ diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index f7d5bef50..3f8b3c7b4 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -7,14 +7,76 @@ * configuration (hostapd) is handled by hardware.c. */ +#include + #include #include #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); diff --git a/src/confd/src/if-wireguard.c b/src/confd/src/if-wireguard.c index f4ca46403..90b5958b2 100644 --- a/src/confd/src/if-wireguard.c +++ b/src/confd/src/if-wireguard.c @@ -2,9 +2,81 @@ #include #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 diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 022b44483..ba1e04a0c 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -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 @@ int interfaces_change(sr_session_ctx_t *session, struct lyd_node *config, struct 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; diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index 97c59098e..44968f0db 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -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_ */ diff --git a/src/confd/src/keystore.c b/src/confd/src/keystore.c index bb7137786..069467485 100644 --- a/src/confd/src/keystore.c +++ b/src/confd/src/keystore.c @@ -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; diff --git a/src/confd/yang/confd/infix-crypto-types.yang b/src/confd/yang/confd/infix-crypto-types.yang index f6b06e926..ab7ab37ab 100644 --- a/src/confd/yang/confd/infix-crypto-types.yang +++ b/src/confd/yang/confd/infix-crypto-types.yang @@ -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."; } } diff --git a/src/confd/yang/confd/infix-if-wireguard.yang b/src/confd/yang/confd/infix-if-wireguard.yang index ea5e2c2d0..7441ce254 100644 --- a/src/confd/yang/confd/infix-if-wireguard.yang +++ b/src/confd/yang/confd/infix-if-wireguard.yang @@ -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"; } } diff --git a/src/confd/yang/confd/infix-keystore.yang b/src/confd/yang/confd/infix-keystore.yang index 9f1f61bd6..c27fbcf81 100644 --- a/src/confd/yang/confd/infix-keystore.yang +++ b/src/confd/yang/confd/infix-keystore.yang @@ -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)"; - - } - } - } } diff --git a/src/klish-plugin-infix/src/infix.c b/src/klish-plugin-infix/src/infix.c index a6515c21e..48a2106df 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 e6b29cbf8..bce73835a 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -155,6 +155,20 @@ + + + + + + + + + + + + + + @@ -259,6 +273,21 @@ + + + +priv=$(wg genkey) +pub=$(printf '%s' "$priv" | wg pubkey) +echo "Private: $priv" +echo "Public: $pub" + + + + + wg genpsk + + + @@ -338,6 +367,20 @@ + + + + + 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 ffd381a4d..488655d7a 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 @@ def print_stats(self): 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 perm_str(has_access, has_restrictions): 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": diff --git a/test/case/interfaces/wireguard_multipoint/test.py b/test/case/interfaces/wireguard_multipoint/test.py index 531192103..c790ae520 100755 --- a/test/case/interfaces/wireguard_multipoint/test.py +++ b/test/case/interfaces/wireguard_multipoint/test.py @@ -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" }] } } diff --git a/utils/ixll b/utils/ixll index f7cdffa70..91a85247a 100755 --- a/utils/ixll +++ b/utils/ixll @@ -22,6 +22,12 @@ may be supplied in places where a hostname is otherwise expected. Commands: + scan [-a] [] + 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 is specified. + peer Return the address of the first IPv6 neighbor to respond on 's local LAN. @@ -75,6 +81,9 @@ case "$cmd" in help) usage && exit 0 ;; + scan) + llscan "$@" + ;; peer) llpeer "$@" ;; diff --git a/utils/libll.sh b/utils/libll.sh index a4441832e..3a049a1e6 100644 --- a/utils/libll.sh +++ b/utils/libll.sh @@ -144,3 +144,155 @@ llscp() $sshpasscmd scp $LLSCP_OPTS "$src" "$dst" } + +# Usage: llscan [-a] [] +# +# 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 + 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 + } + ' +}