Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rpki: T6034: move file based SSH keys for authentication to PKI subsystem #2988

Merged
merged 6 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions interface-definitions/include/pki/openssh-key.xml.i
Original file line number Diff line number Diff line change
@@ -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 -->
39 changes: 39 additions & 0 deletions interface-definitions/pki.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,45 @@
</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>
<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>
Expand Down
17 changes: 1 addition & 16 deletions interface-definitions/protocols_rpki.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,7 @@
<help>RPKI SSH connection settings</help>
</properties>
<children>
<leafNode name="private-key-file">
<properties>
<help>RPKI SSH private key file</help>
<constraint>
<validator name="file-path"/>
</constraint>
</properties>
</leafNode>
<leafNode name="public-key-file">
<properties>
<help>RPKI SSH public key file path</help>
<constraint>
<validator name="file-path"/>
</constraint>
</properties>
</leafNode>
#include <include/pki/openssh-key.xml.i>
#include <include/generic-username.xml.i>
</children>
</node>
Expand Down
31 changes: 27 additions & 4 deletions python/vyos/pki.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,7 +20,9 @@
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
Expand All @@ -45,6 +47,8 @@
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

Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -429,4 +453,3 @@ def ca_cmp(ca_name1, ca_name2, 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)))

80 changes: 56 additions & 24 deletions smoketest/scripts/cli/test_protocols_rpki.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,57 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import unittest

from base_vyostest_shim import VyOSUnitTestSHIM

from vyos.configsession import ConfigSessionError
from vyos.utils.process import cmd
from vyos.utils.process import process_named_running

base_path = ['protocols', 'rpki']
PROCESS_NAME = 'bgpd'

rpki_ssh_key = '/config/auth/id_rsa_rpki'
rpki_ssh_pub = f'{rpki_ssh_key}.pub'
rpki_key_name = 'rpki-smoketest'
rpki_key_type = 'ssh-rsa'

rpki_ssh_key = """
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz
c2gtcnNhAAAAAwEAAQAAAQEAweDyflDFR4qyEwETbJkZ2ZZc+sJNiDTvYpwGsWIk
ju49lJSxHe1xKf8FhwfyMu40Snt1yDlRmmmz4CsbLgbuZGMPvXG11e34+C0pSVUv
pF6aqRTeLl1pDRK7Rnjgm3su+I8SRLQR4qbLG6VXWOFuVpwiqbExLaU0hFYTPNP+
dArNpsWEEKsohk6pTXdhg3VzWp3vCMjl2JTshDa3lD7p2xISSAReEY0fnfEAmQzH
4Z6DIwwGdFuMWoQIg+oFBM9ARrO2/FIjRsz6AecR/WeU72JEw4aJic1/cAJQA6Pi
QBHwkuo3Wll1tbpxeRZoB2NQG22ETyJLvhfTaooNLT9HpQAAA8joU5dM6FOXTAAA
AAdzc2gtcnNhAAABAQDB4PJ+UMVHirITARNsmRnZllz6wk2INO9inAaxYiSO7j2U
lLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV7fj4LSlJVS+kXpqp
FN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSEVhM80/50Cs2m
xYQQqyiGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d8QCZDMfhnoMj
DAZ0W4xahAiD6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9wAlADo+JAEfCS
6jdaWXW1unF5FmgHY1AbbYRPIku+F9Nqig0tP0elAAAAAwEAAQAAAQACkDlUjzfU
htJs6uY5WNrdJB5NmHUS+HQzzxFNlhkapK6+wKqI1UNaRUtq6iF7J+gcFf7MK2nX
S098BsXguWm8fQzPuemoDvHsQhiaJhyvpSqRUrvPTB/f8t/0AhQiKiJIWgfpTaIw
53inAGwjujNNxNm2eafHTThhCYxOkRT7rsT6bnSio6yeqPy5QHg7IKFztp5FXDUy
iOS3aX3SvzQcDUkMXALdvzX50t1XIk+X48Rgkq72dL4VpV2oMNDu3hM6FqBUplf9
Mv3s51FNSma/cibCQoVufrIfoqYjkNTjIpYFUcq4zZ0/KvgXgzSsy9VN/4Ttbalr
Ouu7X/SHJbvhAAAAgGPFsXgONYQvXxCnK1dIueozgaZg1I/n522E2ZCOXBW4dYJV
yNpppwRreDzuFzTDEe061MpNHfScjVBJCCulivFYWscL6oaGsryDbFxO3QmB4I98
UBqrds2yan9/JGc6EYe299yvaHy7Y64+NC0+fN8H2RAZ61T4w10JrCaJRyvzAAAA
gQDvBfuV1U7o9k/fbU+U7W2UYnWblpOZAMfi1XQP6IJJeyWs90PdTdXh+l0eIQrC
awIiRJytNfxMmbD4huwTf77fWiyCcPznmALQ7ex/yJ+W5Z0V4dPGF3h7o1uiS236
JhQ7mfcliCkhp/1PIklBIMPcCp0zl+s9wMv2hX7w1Pah9QAAAIEAz6YgU9Xute+J
+dBwoWxEQ+igR6KE55Um7O9AvSrqnCm9r7lSFsXC2ErYOxoDSJ3yIBEV0b4XAGn6
tbbVIs3jS8BnLHxclAHQecOx1PGn7PKbnPW0oJRq/X9QCIEelKYvlykpayn7uZoo
TXqcDaPZxfPpmPdye8chVJvdygi7kPEAAAAMY3BvQExSMS53dWUzAQIDBAUGBw==
"""

rpki_ssh_pub = """
AAAAB3NzaC1yc2EAAAADAQABAAABAQDB4PJ+UMVHirITARNsmRnZllz6wk2INO9i
nAaxYiSO7j2UlLEd7XEp/wWHB/Iy7jRKe3XIOVGaabPgKxsuBu5kYw+9cbXV7fj4
LSlJVS+kXpqpFN4uXWkNErtGeOCbey74jxJEtBHipssbpVdY4W5WnCKpsTEtpTSE
VhM80/50Cs2mxYQQqyiGTqlNd2GDdXNane8IyOXYlOyENreUPunbEhJIBF4RjR+d
8QCZDMfhnoMjDAZ0W4xahAiD6gUEz0BGs7b8UiNGzPoB5xH9Z5TvYkTDhomJzX9w
AlADo+JAEfCS6jdaWXW1unF5FmgHY1AbbYRPIku+F9Nqig0tP0el
"""

class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
@classmethod
Expand All @@ -44,10 +81,6 @@ def tearDown(self):
self.cli_delete(base_path)
self.cli_commit()

# Nothing RPKI specific should be left over in the config
# frrconfig = self.getFRRconfig('rpki')
# self.assertNotIn('rpki', frrconfig)

# check process health and continuity
self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME))

Expand Down Expand Up @@ -107,14 +140,17 @@ def test_rpki_ssh(self):
},
}

self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key.replace('\n','')])
self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub.replace('\n','')])
self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'type', rpki_key_type])

self.cli_set(base_path + ['polling-period', polling])

for peer, peer_config in cache.items():
self.cli_set(base_path + ['cache', peer, 'port', peer_config['port']])
self.cli_set(base_path + ['cache', peer, 'preference', peer_config['preference']])
self.cli_set(base_path + ['cache', peer, 'ssh', 'username', peer_config['username']])
self.cli_set(base_path + ['cache', peer, 'ssh', 'public-key-file', rpki_ssh_pub])
self.cli_set(base_path + ['cache', peer, 'ssh', 'private-key-file', rpki_ssh_key])
for cache_name, cache_config in cache.items():
self.cli_set(base_path + ['cache', cache_name, 'port', cache_config['port']])
self.cli_set(base_path + ['cache', cache_name, 'preference', cache_config['preference']])
self.cli_set(base_path + ['cache', cache_name, 'ssh', 'username', cache_config['username']])
self.cli_set(base_path + ['cache', cache_name, 'ssh', 'key', rpki_key_name])

# commit changes
self.cli_commit()
Expand All @@ -123,12 +159,13 @@ def test_rpki_ssh(self):
frrconfig = self.getFRRconfig('rpki')
self.assertIn(f'rpki polling_period {polling}', frrconfig)

for peer, peer_config in cache.items():
port = peer_config['port']
preference = peer_config['preference']
username = peer_config['username']
self.assertIn(f'rpki cache {peer} {port} {username} {rpki_ssh_key} {rpki_ssh_pub} preference {preference}', frrconfig)
for cache_name, cache_config in cache.items():
port = cache_config['port']
preference = cache_config['preference']
username = cache_config['username']
self.assertIn(f'rpki cache {cache_name} {port} {username} /run/frr/id_rpki_{cache_name} /run/frr/id_rpki_{cache_name}.pub preference {preference}', frrconfig)

self.cli_delete(['pki', 'openssh'])

def test_rpki_verify_preference(self):
cache = {
Expand All @@ -150,10 +187,5 @@ def test_rpki_verify_preference(self):
with self.assertRaises(ConfigSessionError):
self.cli_commit()


if __name__ == '__main__':
# Create OpenSSH keypair used in RPKI tests
if not os.path.isfile(rpki_ssh_key):
cmd(f'ssh-keygen -t rsa -f {rpki_ssh_key} -N ""')

unittest.main(verbosity=2)
32 changes: 32 additions & 0 deletions src/conf_mode/pki.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
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
Expand Down Expand Up @@ -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':{}})
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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']
Expand Down