summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/include/pki/openssh-key.xml.i14
-rw-r--r--interface-definitions/pki.xml.in39
-rw-r--r--python/vyos/pki.py31
-rwxr-xr-xsrc/conf_mode/pki.py32
4 files changed, 112 insertions, 4 deletions
diff --git a/interface-definitions/include/pki/openssh-key.xml.i b/interface-definitions/include/pki/openssh-key.xml.i
new file mode 100644
index 000000000..8f005d077
--- /dev/null
+++ b/interface-definitions/include/pki/openssh-key.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from pki/openssh-key.xml.i -->
+<leafNode name="key">
+ <properties>
+ <help>OpenSSH key in PKI configuration</help>
+ <completionHelp>
+ <path>pki openssh</path>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Name of OpenSSH key in PKI configuration</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in
index 617bdd584..7a0b073b4 100644
--- a/interface-definitions/pki.xml.in
+++ b/interface-definitions/pki.xml.in
@@ -168,6 +168,45 @@
</properties>
<children>
#include <include/pki/cli-public-key-base64.xml.i>
+ <leafNode name="type">
+ <properties>
+ <help>SSH public key type</help>
+ <completionHelp>
+ <list>ssh-rsa</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ssh-rsa</format>
+ <description>Key pair based on RSA algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>(ssh-rsa)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="private">
+ <properties>
+ <help>Private key</help>
+ </properties>
+ <children>
+ #include <include/pki/cli-private-key-base64.xml.i>
+ #include <include/pki/password-protected.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ <tagNode name="openssh">
+ <properties>
+ <help>OpenSSH public and private keys</help>
+ </properties>
+ <children>
+ <node name="public">
+ <properties>
+ <help>Public key</help>
+ </properties>
+ <children>
+ #include <include/pki/cli-public-key-base64.xml.i>
</children>
</node>
<node name="private">
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index 792e24b76..02dece471 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2023-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -20,7 +20,9 @@ import ipaddress
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.x509.extensions import ExtensionNotFound
-from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, ExtensionOID
+from cryptography.x509.oid import NameOID
+from cryptography.x509.oid import ExtendedKeyUsageOID
+from cryptography.x509.oid import ExtensionOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import dh
@@ -45,6 +47,8 @@ DH_BEGIN='-----BEGIN DH PARAMETERS-----\n'
DH_END='\n-----END DH PARAMETERS-----'
OVPN_BEGIN = '-----BEGIN OpenVPN Static key V{0}-----\n'
OVPN_END = '\n-----END OpenVPN Static key V{0}-----'
+OPENSSH_KEY_BEGIN='-----BEGIN OPENSSH PRIVATE KEY-----\n'
+OPENSSH_KEY_END='\n-----END OPENSSH PRIVATE KEY-----'
# Print functions
@@ -229,6 +233,12 @@ def wrap_public_key(raw_data):
def wrap_private_key(raw_data, passphrase=None):
return (KEY_ENC_BEGIN if passphrase else KEY_BEGIN) + raw_data + (KEY_ENC_END if passphrase else KEY_END)
+def wrap_openssh_public_key(raw_data, type):
+ return f'{type} {raw_data}'
+
+def wrap_openssh_private_key(raw_data):
+ return OPENSSH_KEY_BEGIN + raw_data + OPENSSH_KEY_END
+
def wrap_certificate_request(raw_data):
return CSR_BEGIN + raw_data + CSR_END
@@ -245,7 +255,6 @@ def wrap_openvpn_key(raw_data, version='1'):
return OVPN_BEGIN.format(version) + raw_data + OVPN_END.format(version)
# Load functions
-
def load_public_key(raw_data, wrap_tags=True):
if wrap_tags:
raw_data = wrap_public_key(raw_data)
@@ -267,6 +276,21 @@ def load_private_key(raw_data, passphrase=None, wrap_tags=True):
except ValueError:
return False
+def load_openssh_public_key(raw_data, type):
+ try:
+ return serialization.load_ssh_public_key(bytes(f'{type} {raw_data}', 'utf-8'))
+ except ValueError:
+ return False
+
+def load_openssh_private_key(raw_data, passphrase=None, wrap_tags=True):
+ if wrap_tags:
+ raw_data = wrap_openssh_private_key(raw_data)
+
+ try:
+ return serialization.load_ssh_private_key(bytes(raw_data, 'utf-8'), password=passphrase)
+ except ValueError:
+ return False
+
def load_certificate_request(raw_data, wrap_tags=True):
if wrap_tags:
raw_data = wrap_certificate_request(raw_data)
@@ -429,4 +453,3 @@ def sort_ca_chain(ca_names, pki_node):
from functools import cmp_to_key
return sorted(ca_names, key=cmp_to_key(lambda cert1, cert2: ca_cmp(cert1, cert2, pki_node)))
-
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 4be40e99e..2d076e42d 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -29,6 +29,8 @@ from vyos.defaults import directories
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
from vyos.pki import load_public_key
+from vyos.pki import load_openssh_public_key
+from vyos.pki import load_openssh_private_key
from vyos.pki import load_private_key
from vyos.pki import load_crl
from vyos.pki import load_dh_parameters
@@ -150,6 +152,11 @@ def get_config(config=None):
if 'changed' not in pki: pki.update({'changed':{}})
pki['changed'].update({'key_pair' : tmp})
+ tmp = node_changed(conf, base + ['openssh'], recursive=True)
+ if tmp:
+ if 'changed' not in pki: pki.update({'changed':{}})
+ pki['changed'].update({'openssh' : tmp})
+
tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True)
if tmp:
if 'changed' not in pki: pki.update({'changed':{}})
@@ -241,6 +248,17 @@ def is_valid_private_key(raw_data, protected=False):
return True
return load_private_key(raw_data, passphrase=None, wrap_tags=True)
+def is_valid_openssh_public_key(raw_data, type):
+ # If it loads correctly we're good, or return False
+ return load_openssh_public_key(raw_data, type)
+
+def is_valid_openssh_private_key(raw_data, protected=False):
+ # If it loads correctly we're good, or return False
+ # With encrypted private keys, we always return true as we cannot ask for password to verify
+ if protected:
+ return True
+ return load_openssh_private_key(raw_data, passphrase=None, wrap_tags=True)
+
def is_valid_crl(raw_data):
# If it loads correctly we're good, or return False
return load_crl(raw_data, wrap_tags=True)
@@ -322,6 +340,20 @@ def verify(pki):
if not is_valid_private_key(private['key'], protected):
raise ConfigError(f'Invalid private key on key-pair "{name}"')
+ if 'openssh' in pki:
+ for name, key_conf in pki['openssh'].items():
+ if 'public' in key_conf and 'key' in key_conf['public']:
+ if 'type' not in key_conf['public']:
+ raise ConfigError(f'Must define OpenSSH public key type for "{name}"')
+ if not is_valid_openssh_public_key(key_conf['public']['key'], key_conf['public']['type']):
+ raise ConfigError(f'Invalid OpenSSH public key "{name}"')
+
+ if 'private' in key_conf and 'key' in key_conf['private']:
+ private = key_conf['private']
+ protected = 'password_protected' in private
+ if not is_valid_openssh_private_key(private['key'], protected):
+ raise ConfigError(f'Invalid OpenSSH private key "{name}"')
+
if 'x509' in pki:
if 'default' in pki['x509']:
default_values = pki['x509']['default']