From 71397fb4070758e5ce29be08a3572bf3ee62cb06 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Sun, 11 Feb 2024 21:49:25 +0100 Subject: pki: T6034: add OpenSSH key support set pki openssh rpki private key ... set pki openssh rpki public key ... set pki openssh rpki public type 'ssh-rsa' (cherry picked from commit 8c78ef0879f22ffd4a5f7fdb175e9109b46e9d7b) --- .../include/pki/openssh-key.xml.i | 14 ++++++++ interface-definitions/pki.xml.in | 39 ++++++++++++++++++++++ python/vyos/pki.py | 31 ++++++++++++++--- src/conf_mode/pki.py | 32 ++++++++++++++++++ 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 interface-definitions/include/pki/openssh-key.xml.i 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 @@ + + + + OpenSSH key in PKI configuration + + pki openssh + + + txt + Name of OpenSSH key in PKI configuration + + + + 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 @@ -157,6 +157,45 @@ + + + OpenSSH public and private keys + + + + + Public key + + + #include + + + SSH public key type + + ssh-rsa + + + ssh-rsa + Key pair based on RSA algorithm + + + (ssh-rsa) + + + + + + + + Private key + + + #include + #include + + + + OpenSSH public and private keys 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'] -- cgit v1.2.3