From 68a420ff00eb51bbd14f119bdbcf6faafdac39ec Mon Sep 17 00:00:00 2001 From: Joe Testa Date: Wed, 15 Jul 2020 14:32:14 -0400 Subject: [PATCH] Added policy support for optional host key types, like certificates and smart card-based types. --- policies/ubuntu_server_16_04.txt | 5 ++++- policies/ubuntu_server_18_04.txt | 5 ++++- policies/ubuntu_server_20_04.txt | 5 ++++- ssh-audit.py | 19 +++++++++++++++---- .../openssh_5.6p1_policy_test3.json | 2 +- .../openssh_5.6p1_policy_test3.txt | 2 +- test/test_policy.py | 2 +- 7 files changed, 30 insertions(+), 10 deletions(-) diff --git a/policies/ubuntu_server_16_04.txt b/policies/ubuntu_server_16_04.txt index 1b39972..5bb2e8b 100644 --- a/policies/ubuntu_server_16_04.txt +++ b/policies/ubuntu_server_16_04.txt @@ -2,7 +2,7 @@ # Official policy for hardened OpenSSH on Ubuntu Server 16.04 LTS. # -name = "Ubuntu Server 16.04 LTS" +name = "Hardened Ubuntu Server 16.04 LTS" version = 1 # Group exchange DH modulus sizes. @@ -11,6 +11,9 @@ dh_modulus_size_diffie-hellman-group-exchange-sha256 = 2048 # The host key types that must match exactly (order matters). host keys = ssh-ed25519 +# Host key types that may optionally appear. +optional host keys = sk-ssh-ed25519@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com + # The key exchange algorithms that must match exactly (order matters). key exchanges = curve25519-sha256@libssh.org, diffie-hellman-group-exchange-sha256 diff --git a/policies/ubuntu_server_18_04.txt b/policies/ubuntu_server_18_04.txt index c23d5e7..cf936f4 100644 --- a/policies/ubuntu_server_18_04.txt +++ b/policies/ubuntu_server_18_04.txt @@ -2,7 +2,7 @@ # Official policy for hardened OpenSSH on Ubuntu Server 18.04 LTS. # -name = "Ubuntu Server 18.04 LTS" +name = "Hardened Ubuntu Server 18.04 LTS" version = 1 # Group exchange DH modulus sizes. @@ -11,6 +11,9 @@ dh_modulus_size_diffie-hellman-group-exchange-sha256 = 2048 # The host key types that must match exactly (order matters). host keys = ssh-ed25519 +# Host key types that may optionally appear. +optional host keys = sk-ssh-ed25519@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com + # The key exchange algorithms that must match exactly (order matters). key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group-exchange-sha256 diff --git a/policies/ubuntu_server_20_04.txt b/policies/ubuntu_server_20_04.txt index bce0654..b1d4a57 100644 --- a/policies/ubuntu_server_20_04.txt +++ b/policies/ubuntu_server_20_04.txt @@ -2,7 +2,7 @@ # Official policy for hardened OpenSSH on Ubuntu Server 20.04 LTS. # -name = "Ubuntu Server 20.04 LTS" +name = "Hardened Ubuntu Server 20.04 LTS" version = 1 # RSA host key sizes. @@ -15,6 +15,9 @@ dh_modulus_size_diffie-hellman-group-exchange-sha256 = 2048 # The host key types that must match exactly (order matters). host keys = rsa-sha2-512, rsa-sha2-256, ssh-ed25519 +# Host key types that may optionally appear. +optional host keys = sk-ssh-ed25519@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com + # The key exchange algorithms that must match exactly (order matters). key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group-exchange-sha256 diff --git a/ssh-audit.py b/ssh-audit.py index 56a6cc7..4f53542 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -101,6 +101,7 @@ class Policy: self._banner = None # type: Optional[str] self._compressions = None # type: Optional[List[str]] self._host_keys = None # type: Optional[List[str]] + self._optional_host_keys = None # type: Optional[List[str]] self._kex = None # type: Optional[List[str]] self._ciphers = None # type: Optional[List[str]] self._macs = None # type: Optional[List[str]] @@ -137,7 +138,7 @@ class Policy: key = key.strip() val = val.strip() - if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'key exchanges', 'ciphers', 'macs', 'client policy'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'): + if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs', 'client policy'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'): raise ValueError("invalid field found in policy: %s" % line) if key in ['name', 'banner']: @@ -158,7 +159,7 @@ class Policy: self._banner = val elif key == 'version': self._version = val - elif key in ['compressions', 'host keys', 'key exchanges', 'ciphers', 'macs']: + elif key in ['compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs']: try: algs = val.split(',') except ValueError: @@ -172,6 +173,8 @@ class Policy: self._compressions = algs elif key == 'host keys': self._host_keys = algs + elif key == 'optional host keys': + self._optional_host_keys = algs elif key == 'key exchanges': self._kex = algs elif key == 'ciphers': @@ -273,6 +276,9 @@ version = 1 # The host key types that must match exactly (order matters). host keys = %s +# Host key types that may optionally appear. +#optional host keys = ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com + # The key exchange algorithms that must match exactly (order matters). key exchanges = %s @@ -305,9 +311,14 @@ macs = %s ret = False errors.append('Compression types did not match. Expected: %s; Actual: %s' % (self._compressions, kex.server.compression)) - if (self._host_keys is not None) and (kex.key_algorithms != self._host_keys): + # If a list of optional host keys was given in the policy, remove any of its entries from the list retrieved from the server. This allows us to do an exact comparison with the expected list below. + pruned_host_keys = kex.key_algorithms + if self._optional_host_keys is not None: + pruned_host_keys = [x for x in kex.key_algorithms if x not in self._optional_host_keys] + + if (self._host_keys is not None) and (pruned_host_keys != self._host_keys): ret = False - errors.append('Host key types did not match. Expected: %s; Actual: %s' % (self._host_keys, kex.key_algorithms)) + errors.append('Host key types did not match. Expected (required): %s; Expected (optional): %s; Actual: %s' % (self._host_keys, self._optional_host_keys, kex.key_algorithms)) if self._hostkey_sizes is not None: hostkey_types = list(self._hostkey_sizes.keys()) diff --git a/test/docker/expected_results/openssh_5.6p1_policy_test3.json b/test/docker/expected_results/openssh_5.6p1_policy_test3.json index 3942903..1be54ce 100644 --- a/test/docker/expected_results/openssh_5.6p1_policy_test3.json +++ b/test/docker/expected_results/openssh_5.6p1_policy_test3.json @@ -1 +1 @@ -{"errors": ["Host key types did not match. Expected: ['ssh-rsa', 'ssh-dss', 'key_alg1']; Actual: ['ssh-rsa', 'ssh-dss']"], "host": "localhost", "passed": false, "policy": "Docker policy: test3 (version 1)"} +{"errors": ["Host key types did not match. Expected (required): ['ssh-rsa', 'ssh-dss', 'key_alg1']; Expected (optional): None; Actual: ['ssh-rsa', 'ssh-dss']"], "host": "localhost", "passed": false, "policy": "Docker policy: test3 (version 1)"} diff --git a/test/docker/expected_results/openssh_5.6p1_policy_test3.txt b/test/docker/expected_results/openssh_5.6p1_policy_test3.txt index 2ff5b1a..3117c7b 100644 --- a/test/docker/expected_results/openssh_5.6p1_policy_test3.txt +++ b/test/docker/expected_results/openssh_5.6p1_policy_test3.txt @@ -3,4 +3,4 @@ Policy: Docker policy: test3 (version 1) Result: ❌ Failed!  Errors: - * Host key types did not match. Expected: ['ssh-rsa', 'ssh-dss', 'key_alg1']; Actual: ['ssh-rsa', 'ssh-dss'] + * Host key types did not match. Expected (required): ['ssh-rsa', 'ssh-dss', 'key_alg1']; Expected (optional): None; Actual: ['ssh-rsa', 'ssh-dss'] diff --git a/test/test_policy.py b/test/test_policy.py index e262899..cd772d7 100644 --- a/test/test_policy.py +++ b/test/test_policy.py @@ -192,7 +192,7 @@ macs = mac_alg1, mac_alg2, mac_alg3''' pol_data = pol_data.replace(date.today().strftime('%Y/%m/%d'), '[todays date]') # Instead of writing out the entire expected policy--line by line--just check that it has the expected hash. - assert hashlib.sha256(pol_data.encode('ascii')).hexdigest() == '1765f236d765b1741e7006f601babf0a8e1628326341a3a00b1026c7f85f48ce' + assert hashlib.sha256(pol_data.encode('ascii')).hexdigest() == '4af7777fb57a1dad0cf438c899a11d4f625fd9276ea3bb5ef5c9fe8806cb47dc' def test_policy_evaluate_passing_1(self):