diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..a367a30 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,37 @@ +version: '1.7.1.dev.{build}' + +build: off +branches: + only: + - master + - develop + +environment: + matrix: + - PYTHON: "C:\\Python26" + - PYTHON: "C:\\Python26-x64" + - PYTHON: "C:\\Python27" + - PYTHON: "C:\\Python27-x64" + - PYTHON: "C:\\Python33" + - PYTHON: "C:\\Python33-x64" + - PYTHON: "C:\\Python34" + - PYTHON: "C:\\Python34-x64" + - PYTHON: "C:\\Python35" + - PYTHON: "C:\\Python35-x64" + - PYTHON: "C:\\Python36" + - PYTHON: "C:\\Python36-x64" +matrix: + fast_finish: true + +cache: + - '%LOCALAPPDATA%\pip\Cache' + - .downloads -> .appveyor.yml + +install: + - "cmd /c .\\test\\tools\\ci-win.cmd install" + +test_script: + - "cmd /c .\\test\\tools\\ci-win.cmd test" + +on_failure: + - ps: get-content .tox\*\log\* diff --git a/.gitignore b/.gitignore index 481cc4a..18e72f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ *~ *.pyc -html/ -venv/ -.cache/ \ No newline at end of file +venv*/ +.cache/ +.tox +.coverage* +reports/ +.scannerwork/ +pypi/sshaudit/LICENSE +pypi/sshaudit/README.md +pypi/sshaudit/sshaudit.py diff --git a/.travis.yml b/.travis.yml index f1ee663..08daa94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,80 @@ language: python -python: - - 2.6 - - 2.7 - - 3.3 - - 3.4 - - 3.5 - - pypy - - pypy3 -install: - - pip install --upgrade pytest - - pip install --upgrade pytest-cov - - pip install --upgrade coveralls -script: - - py.test --cov-report= --cov=ssh-audit -v test -after_success: - - coveralls +sudo: false +matrix: + include: + # (default) + - os: linux + python: 2.6 + - os: linux + python: 2.7 + env: SQ=1 + - os: linux + python: 3.3 + - os: linux + python: 3.4 + - os: linux + python: 3.5 + - os: linux + python: 3.6 + - os: linux + python: pypy + - os: linux + python: pypy3 + - os: linux + python: 3.7-dev + # Ubuntu 12.04 + - os: linux + dist: precise + language: generic + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 PY_ORIGIN=pyenv + # Ubuntu 14.04 + - os: linux + dist: trusty + language: generic + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 PY_ORIGIN=pyenv + # macOS 10.12 Sierra + - os: osx + osx_image: xcode8.3 + language: generic + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 + # Mac OS X 10.11 El Capitan + - os: osx + osx_image: xcode7.3 + language: generic + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 + # Mac OS X 10.10 Yosemite + - os: osx + osx_image: xcode6.4 + language: generic + env: PY_VER=py26,py27,py33,py34,py35,py36,pypy,pypy3 + allow_failures: + # PyPy3 on Travis CI is out of date + - python: pypy3 + # Python nightly could fail + - python: 3.7-dev + - env: PY_VER=py37 + - env: PY_VER=py37/pyenv + - env: PY_VER=py37 PY_ORIGIN=pyenv + fast_finish: true +cache: + - pip + - directories: + - $HOME/.pyenv.cache + - $HOME/.bin + +before_install: + - source test/tools/ci-linux.sh + - ci_step_before_install + +install: + - ci_step_install + +script: + - ci_step_script + +after_success: + - ci_step_success + +after_failure: + - ci_step_failure diff --git a/LICENSE b/LICENSE index 0eb1032..4c9f264 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ The MIT License (MIT) -Copyright (C) 2016 Andris Raugulis (moo@arthepsy.eu) +Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) +Copyright (C) 2017-2019 Joe Testa (jtesta@positronsecurity.com) + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e9f8f13..e4e503d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ # ssh-audit -[![build status](https://api.travis-ci.org/arthepsy/ssh-audit.svg)](https://travis-ci.org/arthepsy/ssh-audit) -[![coverage status](https://coveralls.io/repos/github/arthepsy/ssh-audit/badge.svg)](https://coveralls.io/github/arthepsy/ssh-audit) -**ssh-audit** is a tool for ssh server auditing. + +**ssh-audit** is a tool for ssh server & client configuration auditing. ## Features - SSH1 and SSH2 protocol server support; +- analyze SSH client configuration; - grab banner, recognize device or software and operating system, detect compression; - gather key-exchange, host-key, encryption and message authentication code algorithms; - output algorithm information (available since, removed/disabled, unsafe/weak/legacy, etc); @@ -12,11 +17,11 @@ - output security information (related issues, assigned CVE list, etc); - analyze SSH version compatibility based on algorithm information; - historical information from OpenSSH, Dropbear SSH and libssh; -- no dependencies, compatible with Python 2.6+, Python 3.x and PyPy; +- no dependencies ## Usage ``` -usage: ssh-audit.py [-1246pbnvl] +usage: ssh-audit.py [-1246pbcnvlt] -1, --ssh1 force ssh version 1 only -2, --ssh2 force ssh version 2 only @@ -24,19 +29,45 @@ usage: ssh-audit.py [-1246pbnvl] -6, --ipv6 enable IPv6 (order of precedence) -p, --port= port to connect -b, --batch batch output + -c, --client-audit starts a server on port 2222 to audit client + software config (use -p to change port) -n, --no-colors disable colors -v, --verbose verbose output -l, --level= minimum output level (info|warn|fail) - + -t, --timeout= timeout (in seconds) for connection and reading + (default: 5) ``` * if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`. * batch flag `-b` will output sections without header and without empty lines (implies verbose flag). * verbose flag `-v` will prefix each line with section type and algorithm name. -### example -![screenshot](https://cloud.githubusercontent.com/assets/7356025/19233757/3e09b168-8ef0-11e6-91b4-e880bacd0b8a.png) +### Server Audit Example +![screenshot](https://user-images.githubusercontent.com/2982011/64388792-317e6f80-d00e-11e9-826e-a4934769bb07.png) + +### Client Audit Example +TODO ## ChangeLog +### v2.1.0 (???) + - Added client software auditing functionality (see `-c` / `--client-audit` option). + - Fixed crash while scanning Solaris Sun_SSH. + - Added 9 new key exchanges: `gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `gss-group14-sha1-`, `gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==`, `gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==`, `gss-group15-sha512-toWM5Slw5Ew8Mqkay+al2g==`, `diffie-hellman-group15-sha256`, `ecdh-sha2-1.3.132.0.10`, `curve448-sha512`. + - Added 1 new host key type: `ecdsa-sha2-1.3.132.0.10`. + - Added 4 new ciphers: `idea-cbc`, `serpent128-cbc`, `serpent192-cbc`, `serpent256-cbc`. + - Added 6 new MACs: `hmac-sha2-256-96-etm@openssh.com`, `hmac-sha2-512-96-etm@openssh.com`, `hmac-ripemd`, `hmac-sha256-96@ssh.com`, `umac-32@openssh.com`, `umac-96@openssh.com`. + +### v2.0.0 (2019-08-29) + - Forked from https://github.com/arthepsy/ssh-audit (development was stalled, and developer went MIA). + - Added RSA host key length test. + - Added RSA certificate key length test. + - Added Diffie-Hellman modulus size test. + - Now outputs host key fingerprints for RSA and ED25519. + - Added 5 new key exchanges: `sntrup4591761x25519-sha512@tinyssh.org`, `diffie-hellman-group-exchange-sha256@ssh.com`, `diffie-hellman-group-exchange-sha512@ssh.com`, `diffie-hellman-group16-sha256`, `diffie-hellman-group17-sha512`. + - Added 3 new encryption algorithms: `des-cbc-ssh1`, `blowfish-ctr`, `twofish-ctr`. + - Added 10 new MACs: `hmac-sha2-56`, `hmac-sha2-224`, `hmac-sha2-384`, `hmac-sha3-256`, `hmac-sha3-384`, `hmac-sha3-512`, `hmac-sha256`, `hmac-sha256@ssh.com`, `hmac-sha512`, `hmac-512@ssh.com`. + - Added command line argument (-t / --timeout) for connection & reading timeouts. + - Updated CVEs for libssh & Dropbear. + ### v1.7.0 (2016-10-26) - implement options to allow specify IPv4/IPv6 usage and order of precedence - implement option to specify remote port (old behavior kept for compatibility) diff --git a/docker_test.sh b/docker_test.sh new file mode 100755 index 0000000..fa437a7 --- /dev/null +++ b/docker_test.sh @@ -0,0 +1,490 @@ +#!/bin/bash + +# +# This script will set up a docker image with multiple versions of OpenSSH, then +# use it to run tests. +# +# For debugging purposes, here is a cheat sheet for manually running the docker image: +# +# docker run -p 2222:22 -it ssh-audit-test:X /bin/bash +# docker run -p 2222:22 --security-opt seccomp:unconfined -it ssh-audit-test /debug.sh +# docker run -d -p 2222:22 ssh-audit-test:X /openssh/sshd-5.6p1 -D -f /etc/ssh/sshd_config-5.6p1_test1 +# docker run -d -p 2222:22 ssh-audit-test:X /openssh/sshd-8.0p1 -D -f /etc/ssh/sshd_config-8.0p1_test1 +# + + +# This is the docker tag for the image. If this tag doesn't exist, then we assume the +# image is out of date, and generate a new one with this tag. +IMAGE_VERSION=3 + +# This is the name of our docker image. +IMAGE_NAME=ssh-audit-test + + +# Terminal colors. +CLR="\033[0m" +RED="\033[0;31m" +GREEN="\033[0;32m" +REDB="\033[1;31m" # Red + bold +GREENB="\033[1;32m" # Green + bold + + +# Returns 0 if current docker image exists. +function check_if_docker_image_exists { + images=`docker image ls | egrep "$IMAGE_NAME[[:space:]]+$IMAGE_VERSION"` +} + + +# Uncompresses and compiles the specified version of Dropbear. +function compile_dropbear { + version=$1 + compile 'Dropbear' $version +} + + +# Uncompresses and compiles the specified version of OpenSSH. +function compile_openssh { + version=$1 + compile 'OpenSSH' $version +} + + +# Uncompresses and compiles the specified version of TinySSH. +function compile_tinyssh { + version=$1 + compile 'TinySSH' $version +} + + +function compile { + project=$1 + version=$2 + + tarball= + uncompress_options= + source_dir= + server_executable= + if [[ $project == 'OpenSSH' ]]; then + tarball="openssh-${version}.tar.gz" + uncompress_options="xzf" + source_dir="openssh-${version}" + server_executable=sshd + elif [[ $project == 'Dropbear' ]]; then + tarball="dropbear-${version}.tar.bz2" + uncompress_options="xjf" + source_dir="dropbear-${version}" + server_executable=dropbear + elif [[ $project == 'TinySSH' ]]; then + tarball="${version}.tar.gz" + uncompress_options="xzf" + source_dir="tinyssh-${version}" + server_executable='build/bin/tinysshd' + fi + + echo "Uncompressing ${project} ${version}..." + tar $uncompress_options $tarball + + echo "Compiling ${project} ${version}..." + pushd $source_dir > /dev/null + + # TinySSH has no configure script... only a Makefile. + if [[ $project == 'TinySSH' ]]; then + make -j 10 + else + ./configure && make -j 10 + fi + + if [[ ! -f $server_executable ]]; then + echo -e "${REDB}Error: ${server_executable} not built!${CLR}" + exit 1 + fi + + echo -e "\n${GREEN}Successfully built ${project} ${version}${CLR}\n" + popd > /dev/null +} + + +# Creates a new docker image. +function create_docker_image { + # Create a new temporary directory. + TMP_DIR=`mktemp -d /tmp/sshaudit-docker-XXXXXXXXXX` + + # Copy the Dockerfile and all files in the test/docker/ dir to our new temp directory. + find test/docker/ -maxdepth 1 -type f | xargs cp -t $TMP_DIR + + # Make the temp directory our working directory for the duration of the build + # process. + pushd $TMP_DIR > /dev/null + + # Get the release keys. + get_dropbear_release_key + get_openssh_release_key + get_tinyssh_release_key + + # Aside from checking the GPG signatures, we also compare against this known-good + # SHA-256 hash just in case. + get_openssh '4.0p1' '5adb9b2c2002650e15216bf94ed9db9541d9a17c96fcd876784861a8890bc92b' + get_openssh '5.6p1' '538af53b2b8162c21a293bb004ae2bdb141abd250f61b4cea55244749f3c6c2b' + get_openssh '8.0p1' 'bd943879e69498e8031eb6b7f44d08cdc37d59a7ab689aa0b437320c3481fd68' + get_dropbear '2019.78' '525965971272270995364a0eb01f35180d793182e63dd0b0c3eb0292291644a4' + get_tinyssh '20190101' '554a9a94e53b370f0cd0c5fbbd322c34d1f695cbcea6a6a32dcb8c9f595b3fea' + + # Compile the versions of OpenSSH. + compile_openssh '4.0p1' + compile_openssh '5.6p1' + compile_openssh '8.0p1' + + # Compile the versions of Dropbear. + compile_dropbear '2019.78' + + # Compile the versions of TinySSH. + compile_tinyssh '20190101' + + + # Rename the default config files so we know they are our originals. + mv openssh-4.0p1/sshd_config sshd_config-4.0p1_orig + mv openssh-5.6p1/sshd_config sshd_config-5.6p1_orig + mv openssh-8.0p1/sshd_config sshd_config-8.0p1_orig + + + # Create the configurations for each test. + + + # + # OpenSSH v4.0p1 + # + + # Test 1: Basic test. + create_openssh_config '4.0p1' 'test1' "HostKey /etc/ssh/ssh1_host_key\nHostKey /etc/ssh/ssh_host_rsa_key_1024\nHostKey /etc/ssh/ssh_host_dsa_key" + + + # + # OpenSSH v5.6p1 + # + + # Test 1: Basic test. + create_openssh_config '5.6p1' 'test1' "HostKey /etc/ssh/ssh_host_rsa_key_1024\nHostKey /etc/ssh/ssh_host_dsa_key" + + # Test 2: RSA 1024 host key with RSA 1024 certificate. + create_openssh_config '5.6p1' 'test2' "HostKey /etc/ssh/ssh_host_rsa_key_1024\nHostCertificate /etc/ssh/ssh_host_rsa_key_1024-cert_1024.pub" + + # Test 3: RSA 1024 host key with RSA 3072 certificate. + create_openssh_config '5.6p1' 'test3' "HostKey /etc/ssh/ssh_host_rsa_key_1024\nHostCertificate /etc/ssh/ssh_host_rsa_key_1024-cert_3072.pub" + + # Test 4: RSA 3072 host key with RSA 1024 certificate. + create_openssh_config '5.6p1' 'test4' "HostKey /etc/ssh/ssh_host_rsa_key_3072\nHostCertificate /etc/ssh/ssh_host_rsa_key_3072-cert_1024.pub" + + # Test 5: RSA 3072 host key with RSA 3072 certificate. + create_openssh_config '5.6p1' 'test5' "HostKey /etc/ssh/ssh_host_rsa_key_3072\nHostCertificate /etc/ssh/ssh_host_rsa_key_3072-cert_3072.pub" + + + # + # OpenSSH v8.0p1 + # + + # Test 1: Basic test. + create_openssh_config '8.0p1' 'test1' "HostKey /etc/ssh/ssh_host_rsa_key_3072\nHostKey /etc/ssh/ssh_host_ecdsa_key\nHostKey /etc/ssh/ssh_host_ed25519_key" + + # Test 2: ED25519 certificate test. + create_openssh_config '8.0p1' 'test2' "HostKey /etc/ssh/ssh_host_ed25519_key\nHostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub" + + # Test 3: Hardened installation test. + create_openssh_config '8.0p1' 'test3' "HostKey /etc/ssh/ssh_host_ed25519_key\nKexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com" + + + # Now build the docker image! + docker build --tag $IMAGE_NAME:$IMAGE_VERSION . + + popd > /dev/null + rm -rf $TMP_DIR +} + + +# Creates an OpenSSH configuration file for a specific test. +function create_openssh_config { + openssh_version=$1 + test_number=$2 + config_text=$3 + + cp sshd_config-${openssh_version}_orig sshd_config-${openssh_version}_${test_number} + echo -e "${config_text}" >> sshd_config-${openssh_version}_${test_number} +} + + +# Downloads the Dropbear release key and adds it to the local keyring. +function get_dropbear_release_key { + get_release_key 'Dropbear' 'https://matt.ucc.asn.au/dropbear/releases/dropbear-key-2015.asc' 'F29C6773' 'F734 7EF2 EE2E 07A2 6762 8CA9 4493 1494 F29C 6773' +} + + +# Downloads the OpenSSH release key and adds it to the local keyring. +function get_openssh_release_key { + get_release_key 'OpenSSH' 'https://ftp.openbsd.org/pub/OpenBSD/OpenSSH/RELEASE_KEY.asc' '6D920D30' '59C2 118E D206 D927 E667 EBE3 D3E5 F56B 6D92 0D30' +} + + +# Downloads the TinySSH release key and adds it to the local keyring. +function get_tinyssh_release_key { + get_release_key 'TinySSH' '' '96939FF9' 'AADF 2EDF 5529 F170 2772 C8A2 DEC4 D246 931E F49B' +} + + +function get_release_key { + project=$1 + key_url=$2 + key_id=$3 + release_key_fingerprint_expected=$4 + + # The TinySSH release key isn't on any website, apparently. + if [[ $project == 'TinySSH' ]]; then + gpg --recv-key $key_id + else + echo -e "\nGetting ${project} release key...\n" + wget -O key.asc $2 + + echo -e "\nImporting ${project} release key...\n" + gpg --import key.asc + + rm key.asc + fi + + local release_key_fingerprint_actual=`gpg --fingerprint ${key_id}` + if [[ $release_key_fingerprint_actual != *"$release_key_fingerprint_expected"* ]]; then + echo -e "\n${REDB}Error: ${project} release key fingerprint does not match expected value!\n\tExpected: $release_key_fingerprint_expected\n\tActual: $release_key_fingerprint_actual\n\nTerminating.${CLR}" + exit -1 + fi + echo -e "\n\n${GREEN}${project} release key matches expected value.${CLR}\n" +} + + +# Downloads the specified version of Dropbear. +function get_dropbear { + version=$1 + tarball_checksum_expected=$2 + get_source 'Dropbear' $version $tarball_checksum_expected +} + + +# Downloads the specified version of OpenSSH. +function get_openssh { + version=$1 + tarball_checksum_expected=$2 + get_source 'OpenSSH' $version $tarball_checksum_expected +} + + +# Downloads the specified version of TinySSH. +function get_tinyssh { + version=$1 + tarball_checksum_expected=$2 + get_source 'TinySSH' $version $tarball_checksum_expected +} + + +function get_source { + project=$1 + version=$2 + tarball_checksum_expected=$3 + + base_url_source= + base_url_sig= + tarball= + sig= + signer= + if [[ $project == 'OpenSSH' ]]; then + base_url_source='https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/' + base_url_sig=$base_url_source + tarball="openssh-${version}.tar.gz" + sig="${tarball}.asc" + signer="Damien Miller " + elif [[ $project == 'Dropbear' ]]; then + base_url_source='https://matt.ucc.asn.au/dropbear/releases/' + base_url_sig=$base_url_source + tarball="dropbear-${version}.tar.bz2" + sig="${tarball}.asc" + signer="Dropbear SSH Release Signing " + elif [[ $project == 'TinySSH' ]]; then + base_url_source='https://github.com/janmojzis/tinyssh/archive/' + base_url_sig="https://github.com/janmojzis/tinyssh/releases/download/${version}/" + tarball="${version}.tar.gz" + sig="${tarball}.asc" + signer="Jan Mojžíš " + fi + + echo -e "\nGetting ${project} ${version} sources...\n" + wget "${base_url_source}${tarball}" + + echo -e "\nGetting ${project} ${version} signature...\n" + wget "${base_url_sig}${sig}" + + + # Older OpenSSH releases were .sigs. + if [[ ($project == 'OpenSSH') && (! -f $sig) ]]; then + wget ${base_url_sig}openssh-${version}.tar.gz.sig + sig=openssh-${version}.tar.gz.sig + fi + + local gpg_verify=`gpg --verify ${sig} ${tarball} 2>&1` + if [[ $gpg_verify != *"Good signature from \"${signer}"* ]]; then + echo -e "\n\n${REDB}Error: ${project} signature invalid!\n$gpg_verify\n\nTerminating.${CLR}" + exit -1 + fi + + # Check GPG's return value. 0 denotes a valid signature, and 1 is returned + # on invalid signatures. + if [[ $? != 0 ]]; then + echo -e "\n\n${REDB}Error: ${project} signature invalid! Verification returned code: $?\n\nTerminating.${CLR}" + exit -1 + fi + + echo -e "${GREEN}Signature on ${project} sources verified.${CLR}\n" + + local checksum_actual=`sha256sum ${tarball} | cut -f1 -d" "` + if [[ $checksum_actual != $tarball_checksum_expected ]]; then + echo -e "${REDB}Error: ${project} checksum is invalid!\n Expected: ${tarball_checksum_expected}\n Actual: ${checksum_actual}\n\n Terminating.${CLR}" + exit -1 + fi +} + + +# Runs a Dropbear test. Upon failure, a diff between the expected and actual results +# is shown, then the script immediately terminates. +function run_dropbear_test { + dropbear_version=$1 + test_number=$2 + options=$3 + + run_test 'Dropbear' $dropbear_version $test_number "$options" +} + + +# Runs an OpenSSH test. Upon failure, a diff between the expected and actual results +# is shown, then the script immediately terminates. +function run_openssh_test { + openssh_version=$1 + test_number=$2 + + run_test 'OpenSSH' $openssh_version $test_number '' +} + + +# Runs a TinySSH test. Upon failure, a diff between the expected and actual results +# is shown, then the script immediately terminates. +function run_tinyssh_test { + tinyssh_version=$1 + test_number=$2 + + run_test 'TinySSH' $tinyssh_version $test_number '' +} + + +function run_test { + server_type=$1 + version=$2 + test_number=$3 + options=$4 + + server_exec= + test_result= + expected_result= + test_name= + if [[ $server_type == 'OpenSSH' ]]; then + server_exec="/openssh/sshd-${version} -D -f /etc/ssh/sshd_config-${version}_${test_number}" + test_result="${TEST_RESULT_DIR}/openssh_${version}_${test_number}.txt" + expected_result="test/docker/expected_results/openssh_${version}_${test_number}.txt" + test_name="OpenSSH ${version} ${test_number}" + options= + elif [[ $server_type == 'Dropbear' ]]; then + server_exec="/dropbear/dropbear-${version} -F ${options}" + test_result="${TEST_RESULT_DIR}/dropbear_${version}_${test_number}.txt" + expected_result="test/docker/expected_results/dropbear_${version}_${test_number}.txt" + test_name="Dropbear ${version} ${test_number}" + elif [[ $server_type == 'TinySSH' ]]; then + server_exec="/usr/bin/tcpserver -HRDl0 0.0.0.0 22 /tinysshd/tinyssh-20190101 -v /etc/tinyssh/" + test_result="${TEST_RESULT_DIR}/tinyssh_${version}_${test_number}.txt" + expected_result="test/docker/expected_results/tinyssh_${version}_${test_number}.txt" + test_name="TinySSH ${version} ${test_number}" + fi + + cid=`docker run -d -p 2222:22 ${IMAGE_NAME}:${IMAGE_VERSION} ${server_exec}` + if [[ $? != 0 ]]; then + echo -e "${REDB}Failed to run docker image! (exit code: $?)${CLR}" + exit 1 + fi + + ./ssh-audit.py localhost:2222 > $test_result + if [[ $? != 0 ]]; then + echo -e "${REDB}Failed to run ssh-audit.py! (exit code: $?)${CLR}" + docker container stop $cid > /dev/null + exit 1 + fi + + docker container stop $cid > /dev/null + if [[ $? != 0 ]]; then + echo -e "${REDB}Failed to stop docker container ${cid}! (exit code: $?)${CLR}" + exit 1 + fi + + # TinySSH outputs a random string in each banner, which breaks our test. So + # we need to filter out the banner part of the output so we get stable, repeatable + # results. + if [[ $server_type == 'TinySSH' ]]; then + grep -v "(gen) banner: " ${test_result} > "${test_result}.tmp" + mv "${test_result}.tmp" ${test_result} + fi + + diff=`diff -u ${expected_result} ${test_result}` + if [[ $? == 0 ]]; then + echo -e "${test_name} ${GREEN}passed${CLR}." + else + echo -e "${test_name} ${REDB}FAILED${CLR}.\n\n${diff}\n" + exit 1 + fi +} + + + +# First check if docker is functional. +docker version > /dev/null +if [[ $? != 0 ]]; then + echo -e "${REDB}Error: 'docker version' command failed (error code: $?). Is docker installed and functioning?${CLR}" + exit 1 +fi + +# Check if the docker image is the most up-to-date version. If not, create it. +check_if_docker_image_exists +if [[ $? == 0 ]]; then + echo -e "\n${GREEN}Docker image $IMAGE_NAME:$IMAGE_VERSION already exists.${CLR}" +else + echo -e "\nCreating docker image $IMAGE_NAME:$IMAGE_VERSION..." + create_docker_image + echo -e "\n${GREEN}Done creating docker image!${CLR}" +fi + +# Create a temporary directory to write test results to. +TEST_RESULT_DIR=`mktemp -d /tmp/ssh-audit_test-results_XXXXXXXXXX` + +# Now run all the tests. +echo -e "\nRunning tests..." +run_openssh_test '4.0p1' 'test1' +echo +run_openssh_test '5.6p1' 'test1' +run_openssh_test '5.6p1' 'test2' +run_openssh_test '5.6p1' 'test3' +run_openssh_test '5.6p1' 'test4' +run_openssh_test '5.6p1' 'test5' +echo +run_openssh_test '8.0p1' 'test1' +run_openssh_test '8.0p1' 'test2' +run_openssh_test '8.0p1' 'test3' +echo +run_dropbear_test '2019.78' 'test1' '-r /etc/dropbear/dropbear_rsa_host_key_1024 -r /etc/dropbear/dropbear_dss_host_key -r /etc/dropbear/dropbear_ecdsa_host_key' +echo +run_tinyssh_test '20190101' 'test1' + +# The test functions above will terminate the script on failure, so if we reached here, +# all tests are successful. +echo -e "\n${GREENB}ALL TESTS PASS!${CLR}\n" + +rm -rf $TEST_RESULT_DIR +exit 0 diff --git a/pypi/MANIFEST.in b/pypi/MANIFEST.in new file mode 100644 index 0000000..90fc18d --- /dev/null +++ b/pypi/MANIFEST.in @@ -0,0 +1 @@ +include sshaudit/LICENSE diff --git a/pypi/Makefile b/pypi/Makefile new file mode 100644 index 0000000..804b804 --- /dev/null +++ b/pypi/Makefile @@ -0,0 +1,14 @@ +all: + cp ../ssh-audit.py sshaudit/sshaudit.py + cp ../LICENSE sshaudit/LICENSE + cp ../README.md sshaudit/README.md + python3 setup.py sdist bdist_wheel + +uploadtest: + twine upload --repository-url https://test.pypi.org/legacy/ dist/* + +uploadprod: + twine upload dist/* + +clean: + rm -rf build/ dist/ *.egg-info/ sshaudit/sshaudit.py sshaudit/LICENSE sshaudit/README.md diff --git a/pypi/setup.py b/pypi/setup.py new file mode 100644 index 0000000..e9c4c8d --- /dev/null +++ b/pypi/setup.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + + +import re +from setuptools import setup + + +version = re.search('^VERSION\s*=\s*\'v(\d\.\d\.\d)\'', open('sshaudit/sshaudit.py').read(), re.M).group(1) +print("\n\nPackaging ssh-audit v%s...\n\n" % version) + +with open("sshaudit/README.md", "rb") as f: + long_descr = f.read().decode("utf-8") + + +setup( + name = "ssh-audit", + packages = ["sshaudit"], + license = 'MIT', + entry_points = { + "console_scripts": ['ssh-audit = sshaudit.sshaudit:main'] + }, + version = version, + description = "An SSH server configuration security auditing tool", + long_description = long_descr, + long_description_content_type = "text/markdown", + author = "Joe Testa", + author_email = "jtesta@positronsecurity.com", + url = "https://github.com/jtesta/ssh-audit", + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Security", + "Topic :: Security :: Cryptography" + ]) diff --git a/pypi/sshaudit/__init__.py b/pypi/sshaudit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pypi/sshaudit/__main__.py b/pypi/sshaudit/__main__.py new file mode 100644 index 0000000..514349d --- /dev/null +++ b/pypi/sshaudit/__main__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from .sshaudit import main +main() diff --git a/ssh-audit.py b/ssh-audit.py index b25a5e3..6408bcd 100755 --- a/ssh-audit.py +++ b/ssh-audit.py @@ -1,9 +1,10 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ The MIT License (MIT) - Copyright (C) 2016 Andris Raugulis (moo@arthepsy.eu) + Copyright (C) 2017-2019 Joe Testa (jtesta@positronsecurity.com) + Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -24,9 +25,13 @@ THE SOFTWARE. """ from __future__ import print_function -import os, io, sys, socket, struct, random, errno, getopt, re, hashlib, base64 +import base64, binascii, errno, hashlib, getopt, io, os, random, re, select, socket, struct, sys -VERSION = 'v1.7.0' +VERSION = 'v2.1.0-dev' +SSH_HEADER = 'SSH-{0}-OpenSSH_8.0' # SSH software to impersonate + +if sys.version_info.major < 3: + print("\n!!!! NOTE: Python 2 is being considered for deprecation. If you have a good reason to need continued Python 2 support, please e-mail jtesta@positronsecurity.com with your rationale.\n\n") if sys.version_info >= (3,): # pragma: nocover StringIO, BytesIO = io.StringIO, io.BytesIO @@ -39,7 +44,7 @@ else: # pragma: nocover binary_type = str try: # pragma: nocover # pylint: disable=unused-import - from typing import List, Set, Sequence, Tuple, Iterable + from typing import Dict, List, Set, Sequence, Tuple, Iterable from typing import Callable, Optional, Union, Any except ImportError: # pragma: nocover pass @@ -54,10 +59,10 @@ def usage(err=None): # type: (Optional[str]) -> None uout = Output() p = os.path.basename(sys.argv[0]) - uout.head('# {0} {1}, moo@arthepsy.eu\n'.format(p, VERSION)) - if err is not None: + uout.head('# {0} {1}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION)) + if err is not None and len(err) > 0: uout.fail('\n' + err) - uout.info('usage: {0} [-1246pbnvl] \n'.format(p)) + uout.info('usage: {0} [-1246pbcnvlt] \n'.format(p)) uout.info(' -h, --help print this help') uout.info(' -1, --ssh1 force ssh version 1 only') uout.info(' -2, --ssh2 force ssh version 2 only') @@ -65,9 +70,11 @@ def usage(err=None): uout.info(' -6, --ipv6 enable IPv6 (order of precedence)') uout.info(' -p, --port= port to connect') uout.info(' -b, --batch batch output') + uout.info(' -c, --client-audit starts a server on port 2222 to audit client\n software config (use -p to change port)') uout.info(' -n, --no-colors disable colors') uout.info(' -v, --verbose verbose output') uout.info(' -l, --level= minimum output level (info|warn|fail)') + uout.info(' -t, --timeout= timeout (in seconds) for connection and reading\n (default: 5)') uout.sep() sys.exit(1) @@ -81,34 +88,36 @@ class AuditConf(object): self.ssh1 = True self.ssh2 = True self.batch = False + self.client_audit = False self.colors = True self.verbose = False - self.minlevel = 'info' + self.level = 'info' self.ipvo = () # type: Sequence[int] self.ipv4 = False self.ipv6 = False - + self.timeout = 5.0 + def __setattr__(self, name, value): # type: (str, Union[str, int, bool, Sequence[int]]) -> None valid = False - if name in ['ssh1', 'ssh2', 'batch', 'colors', 'verbose']: - valid, value = True, True if value else False + if name in ['ssh1', 'ssh2', 'batch', 'client_audit', 'colors', 'verbose']: + valid, value = True, True if bool(value) else False elif name in ['ipv4', 'ipv6']: valid = False - value = True if value else False + value = True if bool(value) else False ipv = 4 if name == 'ipv4' else 6 if value: value = tuple(list(self.ipvo) + [ipv]) - else: + else: # pylint: disable=else-if-used if len(self.ipvo) == 0: value = (6,) if ipv == 4 else (4,) else: - value = tuple(filter(lambda x: x != ipv, self.ipvo)) + value = tuple([x for x in self.ipvo if x != ipv]) self.__setattr__('ipvo', value) elif name == 'ipvo': if isinstance(value, (tuple, list)): uniq_value = utils.unique_seq(value) - value = tuple(filter(lambda x: x in (4, 6), uniq_value)) + value = tuple([x for x in uniq_value if x in (4, 6)]) valid = True ipv_both = len(value) == 0 object.__setattr__(self, 'ipv4', ipv_both or 4 in value) @@ -118,12 +127,17 @@ class AuditConf(object): if port < 1 or port > 65535: raise ValueError('invalid port: {0}'.format(value)) value = port - elif name in ['minlevel']: + elif name in ['level']: if value not in ('info', 'warn', 'fail'): raise ValueError('invalid level: {0}'.format(value)) valid = True elif name == 'host': valid = True + elif name == 'timeout': + value = utils.parse_float(value) + if value == -1.0: + raise ValueError('invalid timeout: {0}'.format(value)) + valid = True if valid: object.__setattr__(self, name, value) @@ -133,9 +147,9 @@ class AuditConf(object): # pylint: disable=too-many-branches aconf = cls() try: - sopts = 'h1246p:bnvl:' + sopts = 'h1246p:bcnvl:t:' lopts = ['help', 'ssh1', 'ssh2', 'ipv4', 'ipv6', 'port', - 'batch', 'no-colors', 'verbose', 'level='] + 'batch', 'client-audit', 'no-colors', 'verbose', 'level=', 'timeout='] opts, args = getopt.gnu_getopt(args, sopts, lopts) except getopt.GetoptError as err: usage_cb(str(err)) @@ -157,6 +171,8 @@ class AuditConf(object): elif o in ('-b', '--batch'): aconf.batch = True aconf.verbose = True + elif o in ('-c', '--client-audit'): + aconf.client_audit = True elif o in ('-n', '--no-colors'): aconf.colors = False elif o in ('-v', '--verbose'): @@ -164,21 +180,31 @@ class AuditConf(object): elif o in ('-l', '--level'): if a not in ('info', 'warn', 'fail'): usage_cb('level {0} is not valid'.format(a)) - aconf.minlevel = a - if len(args) == 0: + aconf.level = a + elif o in ('-t', '--timeout'): + aconf.timeout = float(a) + if len(args) == 0 and aconf.client_audit == False: usage_cb() - if oport is not None: - host = args[0] - port = utils.parse_int(oport) - else: - s = args[0].split(':') - host = s[0].strip() - if len(s) == 2: - oport, port = s[1], utils.parse_int(s[1]) + if aconf.client_audit == False: + if oport is not None: + host = args[0] else: - oport, port = '22', 22 - if not host: - usage_cb('host is empty') + mx = re.match(r'^\[([^\]]+)\](?::(.*))?$', args[0]) + if bool(mx): + host, oport = mx.group(1), mx.group(2) + else: + s = args[0].split(':') + if len(s) > 2: + host, oport = args[0], '22' + else: + host, oport = s[0], s[1] if len(s) > 1 else '22' + if not host: + usage_cb('host is empty') + else: + host = None + if oport is None: + oport = '2222' + port = utils.parse_int(oport) if port <= 0 or port > 65535: usage_cb('port {0} is not valid'.format(oport)) aconf.host = host @@ -189,29 +215,30 @@ class AuditConf(object): class Output(object): - LEVELS = ['info', 'warn', 'fail'] + LEVELS = ('info', 'warn', 'fail') # type: Sequence[str] COLORS = {'head': 36, 'good': 32, 'warn': 33, 'fail': 31} def __init__(self): # type: () -> None self.batch = False - self.colors = True self.verbose = False - self.__minlevel = 0 + self.use_colors = True + self.__level = 0 + self.__colsupport = 'colorama' in sys.modules or os.name == 'posix' @property - def minlevel(self): + def level(self): # type: () -> str - if self.__minlevel < len(self.LEVELS): - return self.LEVELS[self.__minlevel] + if self.__level < len(self.LEVELS): + return self.LEVELS[self.__level] return 'unknown' - @minlevel.setter - def minlevel(self, name): + @level.setter + def level(self, name): # type: (str) -> None - self.__minlevel = self.getlevel(name) + self.__level = self.get_level(name) - def getlevel(self, name): + def get_level(self, name): # type: (str) -> int cname = 'info' if name == 'good' else name if cname not in self.LEVELS: @@ -226,7 +253,7 @@ class Output(object): @property def colors_supported(self): # type: () -> bool - return 'colorama' in sys.modules or os.name == 'posix' + return self.__colsupport @staticmethod def _colorized(color): @@ -237,9 +264,9 @@ class Output(object): # type: (str) -> Callable[[text_type], None] if name == 'head' and self.batch: return lambda x: None - if not self.getlevel(name) >= self.__minlevel: + if not self.get_level(name) >= self.__level: return lambda x: None - if self.colors and self.colors_supported and name in self.COLORS: + if self.use_colors and self.colors_supported and name in self.COLORS: color = '\033[0;{0}m'.format(self.COLORS[name]) return self._colorized(color) else: @@ -255,7 +282,10 @@ class OutputBuffer(list): sys.stdout = self.__buf return self - def flush(self): + def flush(self, sort_lines=False): + # Lines must be sorted in some cases to ensure consistent testing. + if sort_lines: + self.sort() # type: () -> None for line in self: print(line) @@ -267,6 +297,176 @@ class OutputBuffer(list): class SSH2(object): # pylint: disable=too-few-public-methods + class KexDB(object): # pylint: disable=too-few-public-methods + # pylint: disable=bad-whitespace + WARN_OPENSSH74_UNSAFE = 'disabled (in client) since OpenSSH 7.4, unsafe algorithm' + WARN_OPENSSH72_LEGACY = 'disabled (in client) since OpenSSH 7.2, legacy algorithm' + FAIL_OPENSSH70_LEGACY = 'removed since OpenSSH 7.0, legacy algorithm' + FAIL_OPENSSH70_WEAK = 'removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm' + FAIL_OPENSSH70_LOGJAM = 'disabled (in client) since OpenSSH 7.0, logjam attack' + INFO_OPENSSH69_CHACHA = 'default cipher since OpenSSH 6.9.' + FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm' + FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification' + FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1' + FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67' + FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53' + FAIL_DEPRECATED_CIPHER = 'deprecated cipher' + FAIL_WEAK_CIPHER = 'using weak cipher' + FAIL_PLAINTEXT = 'no encryption/integrity' + FAIL_DEPRECATED_MAC = 'deprecated MAC' + WARN_CURVES_WEAK = 'using weak elliptic curves' + WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' + WARN_MODULUS_SIZE = 'using small 1024-bit modulus' + WARN_HASH_WEAK = 'using weak hashing algorithm' + WARN_CIPHER_MODE = 'using weak cipher mode' + WARN_BLOCK_SIZE = 'using small 64-bit block size' + WARN_CIPHER_WEAK = 'using weak cipher' + WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode' + WARN_TAG_SIZE = 'using small 64-bit tag size' + WARN_TAG_SIZE_96 = 'using small 96-bit tag size' + WARN_EXPERIMENTAL = 'using experimental algorithm' + + ALGORITHMS = { + # Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...]] + 'kex': { + 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], + 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], + 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]], + 'gss-group14-sha1-': [[], [], [WARN_HASH_WEAK]], + 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': [[], [], [WARN_HASH_WEAK]], + 'gss-group14-sha256-toWM5Slw5Ew8Mqkay+al2g==': [[]], + 'gss-group15-sha512-toWM5Slw5Ew8Mqkay+al2g==': [[]], + 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], + 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], + 'diffie-hellman-group15-sha256': [[]], + 'diffie-hellman-group15-sha512': [[]], + 'diffie-hellman-group16-sha256': [[]], + 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], + 'diffie-hellman-group17-sha512': [[]], + 'diffie-hellman-group18-sha512': [['7.3']], + 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], + 'diffie-hellman-group-exchange-sha256': [['4.4']], + 'diffie-hellman-group-exchange-sha256@ssh.com': [[]], + 'diffie-hellman-group-exchange-sha512@ssh.com': [[]], + 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], + 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], + 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], + 'ecdh-sha2-1.3.132.0.10': [[]], # ECDH over secp256k1 (i.e.: the Bitcoin curve) + 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], + 'curve25519-sha256': [['7.4,d2018.76']], + 'curve448-sha512': [[]], + 'kexguess2@matt.ucc.asn.au': [['d2013.57']], + 'rsa1024-sha1': [[], [], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], + 'rsa2048-sha256': [[]], + 'sntrup4591761x25519-sha512@tinyssh.org': [['8.0'], [], [WARN_EXPERIMENTAL]], + 'ext-info-c': [[]], # Extension negotiation (RFC 8308) + 'ext-info-s': [[]], # Extension negotiation (RFC 8308) + }, + 'key': { + 'rsa-sha2-256': [['7.2']], + 'rsa-sha2-512': [['7.2']], + 'ssh-ed25519': [['6.5,l10.7.0']], + 'ssh-ed25519-cert-v01@openssh.com': [['6.5']], + 'ssh-rsa': [['2.5.0,d0.28,l10.2']], + 'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], []], + 'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], + 'ssh-rsa-cert-v01@openssh.com': [['5.6']], + 'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ecdsa-sha2-nistp521-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], + 'ssh-rsa-sha256@ssh.com': [[]], + 'ecdsa-sha2-1.3.132.0.10': [[], [], [WARN_RNDSIG_KEY]], # ECDSA over secp256k1 (i.e.: the Bitcoin curve) + }, + 'enc': { + 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], + 'des-cbc': [[], [FAIL_WEAK_CIPHER], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'des-cbc-ssh1': [[], [FAIL_WEAK_CIPHER], [WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH74_UNSAFE, WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + '3des-ctr': [['d0.52'], [FAIL_WEAK_CIPHER]], + 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'blowfish-ctr': [[], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish192-cbc': [[], [], [WARN_CIPHER_MODE]], + 'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], + 'twofish-ctr': [[]], + 'twofish128-ctr': [['d2015.68']], + 'twofish192-ctr': [[]], + 'twofish256-ctr': [['d2015.68']], + 'serpent128-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], + 'serpent192-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], + 'serpent256-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], + 'serpent128-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'serpent192-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'serpent256-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'idea-cbc': [[], [FAIL_DEPRECATED_CIPHER], [WARN_CIPHER_MODE]], + 'idea-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'cast128-ctr': [[], [FAIL_DEPRECATED_CIPHER]], + 'cast128-cbc': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], + 'arcfour': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], + 'arcfour128': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], + 'arcfour256': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], + 'aes128-cbc': [['2.3.0,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], + 'aes192-cbc': [['2.3.0,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], + 'aes256-cbc': [['2.3.0,d0.47,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], + 'rijndael128-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], + 'rijndael192-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], + 'rijndael256-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], + 'rijndael-cbc@lysator.liu.se': [['2.3.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE]], + 'aes128-ctr': [['3.7,d0.52,l10.4.1']], + 'aes192-ctr': [['3.7,l10.4.1']], + 'aes256-ctr': [['3.7,d0.52,l10.4.1']], + 'aes128-gcm@openssh.com': [['6.2']], + 'aes256-gcm@openssh.com': [['6.2']], + 'chacha20-poly1305@openssh.com': [['6.5'], [], [], [INFO_OPENSSH69_CHACHA]], + }, + 'mac': { + 'none': [['d2013.56'], [FAIL_PLAINTEXT]], + 'hmac-sha1': [['2.1.0,d0.28,l10.2'], [], [WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-sha1-96': [['2.5.0,d0.47', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-sha2-56': [[], [], [WARN_TAG_SIZE, WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-224': [[], [], [WARN_TAG_SIZE, WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-256': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-256-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-384': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-512': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha2-512-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha3-256': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha3-384': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha3-512': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha256': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha256-96@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], + 'hmac-sha256@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha512': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha512@ssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-md5': [['2.1.0,d0.28', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], + 'hmac-ripemd': [[], [FAIL_DEPRECATED_MAC], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], + 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], + 'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], + 'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], + 'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]], + 'hmac-sha1-etm@openssh.com': [['6.2'], [], [WARN_HASH_WEAK]], + 'hmac-sha1-96-etm@openssh.com': [['6.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], + 'hmac-sha2-256-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?). + 'hmac-sha2-512-96-etm@openssh.com': [[], [], [WARN_TAG_SIZE_96]], # Despite the @openssh.com tag, it doesn't appear that this was ever shipped with OpenSSH; it is only implemented in AsyncSSH (?). + 'hmac-sha2-256-etm@openssh.com': [['6.2']], + 'hmac-sha2-512-etm@openssh.com': [['6.2']], + 'hmac-md5-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], + 'hmac-md5-96-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], + 'hmac-ripemd160-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY]], + 'umac-32@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!). + 'umac-64-etm@openssh.com': [['6.2'], [], [WARN_TAG_SIZE]], + 'umac-96@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC]], # Despite having the @openssh.com suffix, this may never have shipped with OpenSSH (!). + 'umac-128-etm@openssh.com': [['6.2']], + } + } # type: Dict[str, Dict[str, List[List[Optional[str]]]]] + class KexParty(object): def __init__(self, enc, mac, compression, languages): # type: (List[text_type], List[text_type], List[text_type], List[text_type]) -> None @@ -305,7 +505,11 @@ class SSH2(object): # pylint: disable=too-few-public-methods self.__server = srv self.__follows = follows self.__unused = unused - + + self.__rsa_key_sizes = {} + self.__dh_modulus_sizes = {} + self.__host_keys = {} + @property def cookie(self): # type: () -> binary_type @@ -342,7 +546,25 @@ class SSH2(object): # pylint: disable=too-few-public-methods def unused(self): # type: () -> int return self.__unused - + + def set_rsa_key_size(self, rsa_type, hostkey_size, ca_size=-1): + self.__rsa_key_sizes[rsa_type] = (hostkey_size, ca_size) + + def rsa_key_sizes(self): + return self.__rsa_key_sizes + + def set_dh_modulus_size(self, gex_alg, modulus_size): + self.__dh_modulus_sizes[gex_alg] = (modulus_size, -1) + + def dh_modulus_sizes(self): + return self.__dh_modulus_sizes + + def set_host_key(self, key_type, hostkey): + self.__host_keys[key_type] = hostkey + + def host_keys(self): + return self.__host_keys + def write(self, wbuf): # type: (WriteBuf) -> None wbuf.write(self.cookie) @@ -388,6 +610,256 @@ class SSH2(object): # pylint: disable=too-few-public-methods kex = cls(cookie, kex_algs, key_algs, cli, srv, follows, unused) return kex + # Obtains host keys, checks their size, and derives their fingerprints. + class HostKeyTest(object): + # Tracks the RSA host key types. As of this writing, testing one in this family yields valid results for the rest. + RSA_FAMILY = ['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512'] + + # Dict holding the host key types we should extract & parse. 'cert' is True to denote that a host key type handles certificates (thus requires additional parsing). 'variable_key_len' is True for host key types that can have variable sizes (True only for RSA types, as the rest are of fixed-size). After the host key type is fully parsed, the key 'parsed' is added with a value of True. + HOST_KEY_TYPES = { + 'ssh-rsa': {'cert': False, 'variable_key_len': True}, + 'rsa-sha2-256': {'cert': False, 'variable_key_len': True}, + 'rsa-sha2-512': {'cert': False, 'variable_key_len': True}, + + 'ssh-rsa-cert-v01@openssh.com': {'cert': True, 'variable_key_len': True}, + + 'ssh-ed25519': {'cert': False, 'variable_key_len': False}, + 'ssh-ed25519-cert-v01@openssh.com': {'cert': True, 'variable_key_len': False}, + } + + @staticmethod + def run(s, server_kex): + KEX_TO_DHGROUP = { + 'diffie-hellman-group1-sha1': KexGroup1, + 'diffie-hellman-group14-sha1': KexGroup14_SHA1, + 'diffie-hellman-group14-sha256': KexGroup14_SHA256, + 'curve25519-sha256': KexCurve25519_SHA256, + 'curve25519-sha256@libssh.org': KexCurve25519_SHA256, + 'diffie-hellman-group16-sha512': KexGroup16_SHA512, + 'diffie-hellman-group18-sha512': KexGroup18_SHA512, + 'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, + 'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, + 'ecdh-sha2-nistp256': KexNISTP256, + 'ecdh-sha2-nistp384': KexNISTP384, + 'ecdh-sha2-nistp521': KexNISTP521, + #'kexguess2@matt.ucc.asn.au': ??? + } + + # Pick the first kex algorithm that the server supports, which we + # happen to support as well. + kex_str = None + kex_group = None + for server_kex_alg in server_kex.kex_algorithms: + if server_kex_alg in KEX_TO_DHGROUP: + kex_str = server_kex_alg + kex_group = KEX_TO_DHGROUP[kex_str]() + break + + if kex_str is not None: + SSH2.HostKeyTest.__test(s, server_kex, kex_str, kex_group, SSH2.HostKeyTest.HOST_KEY_TYPES) + + @staticmethod + def __test(s, server_kex, kex_str, kex_group, host_key_types): + hostkey_modulus_size = 0 + ca_modulus_size = 0 + + # For each host key type... + for host_key_type in host_key_types: + # Skip those already handled (i.e.: those in the RSA family, as testing one tests them all). + if 'parsed' in host_key_types[host_key_type] and host_key_types[host_key_type]['parsed']: + continue + + # If this host key type is supported by the server, we test it. + if host_key_type in server_kex.key_algorithms: + cert = host_key_types[host_key_type]['cert'] + variable_key_len = host_key_types[host_key_type]['variable_key_len'] + + # If the connection is closed, re-open it and get the kex again. + if not s.is_connected(): + s.connect() + unused = None # pylint: disable=unused-variable + unused, unused, err = s.get_banner() + if err is not None: + s.close() + return + + # Parse the server's initial KEX. + packet_type = 0 # pylint: disable=unused-variable + packet_type, payload = s.read_packet() + SSH2.Kex.parse(payload) + + # Send the server our KEXINIT message, using only our + # selected kex and host key type. Send the server's own + # list of ciphers and MACs back to it (this doesn't + # matter, really). + client_kex = SSH2.Kex(os.urandom(16), [kex_str], [host_key_type], server_kex.client, server_kex.server, 0, 0) + + s.write_byte(SSH.Protocol.MSG_KEXINIT) + client_kex.write(s) + s.send_packet() + + # Do the initial DH exchange. The server responds back + # with the host key and its length. Bingo. We also get back the host key fingerprint. + kex_group.send_init(s) + host_key = kex_group.recv_reply(s, variable_key_len) + server_kex.set_host_key(host_key_type, host_key) + + hostkey_modulus_size = kex_group.get_hostkey_size() + ca_modulus_size = kex_group.get_ca_size() + + # Close the socket, as the connection has + # been put in a state that later tests can't use. + s.close() + + # If the host key modulus or CA modulus was successfully parsed, check to see that its a safe size. + if hostkey_modulus_size > 0 or ca_modulus_size > 0: + # Set the hostkey size for all RSA key types since 'ssh-rsa', + # 'rsa-sha2-256', etc. are all using the same host key. + # Note, however, that this may change in the future. + if cert is False and host_key_type in SSH2.HostKeyTest.RSA_FAMILY: + for rsa_type in SSH2.HostKeyTest.RSA_FAMILY: + server_kex.set_rsa_key_size(rsa_type, hostkey_modulus_size) + elif cert is True: + server_kex.set_rsa_key_size(host_key_type, hostkey_modulus_size, ca_modulus_size) + + # Keys smaller than 2048 result in a failure. Update the database accordingly. + if (cert is False) and (hostkey_modulus_size < 2048): + for rsa_type in SSH2.HostKeyTest.RSA_FAMILY: + alg_list = SSH2.KexDB.ALGORITHMS['key'][rsa_type] + alg_list.append(['using small %d-bit modulus' % hostkey_modulus_size]) + elif (cert is True) and ((hostkey_modulus_size < 2048) or (ca_modulus_size > 0 and ca_modulus_size < 2048)): + alg_list = SSH2.KexDB.ALGORITHMS['key'][host_key_type] + min_modulus = min(hostkey_modulus_size, ca_modulus_size) + min_modulus = min_modulus if min_modulus > 0 else max(hostkey_modulus_size, ca_modulus_size) + alg_list.append(['using small %d-bit modulus' % min_modulus]) + + # If this host key type is in the RSA family, then mark them all as parsed (since results in one are valid for them all). + if host_key_type in SSH2.HostKeyTest.RSA_FAMILY: + for rsa_type in SSH2.HostKeyTest.RSA_FAMILY: + host_key_types[rsa_type]['parsed'] = True + else: + host_key_types[host_key_type]['parsed'] = True + + + # Performs DH group exchanges to find what moduli are supported, and checks + # their size. + class GEXTest(object): + + # Creates a new connection to the server. Returns an SSH.Socket, or + # None on failure. + @staticmethod + def reconnect(s, gex_alg): + if s.is_connected(): + return + + s.connect() + unused = None # pylint: disable=unused-variable + unused, unused, err = s.get_banner() + if err is not None: + s.close() + return False + + # Parse the server's initial KEX. + packet_type = 0 # pylint: disable=unused-variable + packet_type, payload = s.read_packet(2) + kex = SSH2.Kex.parse(payload) + + # Send our KEX using the specified group-exchange and most of the + # server's own values. + client_kex = SSH2.Kex(os.urandom(16), [gex_alg], kex.key_algorithms, kex.client, kex.server, 0, 0) + s.write_byte(SSH.Protocol.MSG_KEXINIT) + client_kex.write(s) + s.send_packet() + return True + + # Runs the DH moduli test against the specified target. + @staticmethod + def run(s, kex): + GEX_ALGS = { + 'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, + 'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, + } + + # The previous RSA tests put the server in a state we can't + # test. So we need a new connection to start with a clean + # slate. + if s.is_connected(): + s.close() + + # Check if the server supports any of the group-exchange + # algorithms. If so, test each one. + for gex_alg in GEX_ALGS: + if gex_alg in kex.kex_algorithms: + + if SSH2.GEXTest.reconnect(s, gex_alg) is False: + break + + kex_group = GEX_ALGS[gex_alg]() + smallest_modulus = -1 + + # First try a range of weak sizes. + try: + kex_group.send_init_gex(s, 512, 1024, 1536) + kex_group.recv_reply(s, False) + + # Its been observed that servers will return a group + # larger than the requested max. So just because we + # got here, doesn't mean the server is vulnerable... + smallest_modulus = kex_group.get_dh_modulus_size() + + except Exception as e: # pylint: disable=bare-except + pass + finally: + s.close() + + # Try an array of specific modulus sizes... one at a time. + reconnect_failed = False + for bits in [512, 768, 1024, 1536, 2048, 3072, 4096]: + + # If we found one modulus size already, but we're about + # to test a larger one, don't bother. + if smallest_modulus > 0 and bits >= smallest_modulus: + break + + if SSH2.GEXTest.reconnect(s, gex_alg) is False: + reconnect_failed = True + break + + try: + kex_group.send_init_gex(s, bits, bits, bits) + kex_group.recv_reply(s, False) + smallest_modulus = kex_group.get_dh_modulus_size() + except Exception as e: # pylint: disable=bare-except + #import traceback + #print(traceback.format_exc()) + pass + finally: + # The server is in a state that is not re-testable, + # so there's nothing else to do with this open + # connection. + s.close() + + + if smallest_modulus > 0: + kex.set_dh_modulus_size(gex_alg, smallest_modulus) + + # We flag moduli smaller than 2048 as a failure. + if smallest_modulus < 2048: + text = 'using small %d-bit modulus' % smallest_modulus + lst = SSH2.KexDB.ALGORITHMS['kex'][gex_alg] + # For 'diffie-hellman-group-exchange-sha256', add + # a failure reason. + if len(lst) == 1: + lst.append([text]) + # For 'diffie-hellman-group-exchange-sha1', delete + # the existing failure reason (which is vague), and + # insert our own. + else: + del lst[1] + lst.insert(1, [text]) + + if reconnect_failed: + break class SSH1(object): class CRC32(object): @@ -414,7 +886,7 @@ class SSH1(object): _crc32 = None # type: Optional[SSH1.CRC32] CIPHERS = ['none', 'idea', 'des', '3des', 'tss', 'rc4', 'blowfish'] - AUTHS = [None, 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos'] + AUTHS = ['none', 'rhosts', 'rsa', 'password', 'rhosts_rsa', 'tis', 'kerberos'] @classmethod def crc32(cls, v): @@ -452,13 +924,15 @@ class SSH1(object): 'tis': [['1.2.2']], 'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]], } - } # type: Dict[str, Dict[str, List[List[str]]]] + } # type: Dict[str, Dict[str, List[List[Optional[str]]]]] class PublicKeyMessage(object): def __init__(self, cookie, skey, hkey, pflags, cmask, amask): # type: (binary_type, Tuple[int, int, int], Tuple[int, int, int], int, int, int) -> None - assert len(skey) == 3 - assert len(hkey) == 3 + if len(skey) != 3: + raise ValueError('invalid server key pair: {0}'.format(skey)) + if len(hkey) != 3: + raise ValueError('invalid host key pair: {0}'.format(hkey)) self.__cookie = cookie self.__server_key = skey self.__host_key = hkey @@ -586,8 +1060,8 @@ class ReadBuf(object): def __init__(self, data=None): # type: (Optional[binary_type]) -> None super(ReadBuf, self).__init__() - self._buf = BytesIO(data) if data else BytesIO() - self._len = len(data) if data else 0 + self._buf = BytesIO(data) if data is not None else BytesIO() + self._len = len(data) if data is not None else 0 @property def unread_len(self): @@ -600,7 +1074,8 @@ class ReadBuf(object): def read_byte(self): # type: () -> int - return struct.unpack('B', self.read(1))[0] + v = struct.unpack('B', self.read(1))[0] # type: int + return v def read_bool(self): # type: () -> bool @@ -608,7 +1083,8 @@ class ReadBuf(object): def read_int(self): # type: () -> int - return struct.unpack('>I', self.read(4))[0] + v = struct.unpack('>I', self.read(4))[0] # type: int + return v def read_list(self): # type: () -> List[text_type] @@ -621,13 +1097,13 @@ class ReadBuf(object): return self.read(n) @classmethod - def _parse_mpint(cls, v, pad, sf): + def _parse_mpint(cls, v, pad, f): # type: (binary_type, binary_type, str) -> int r = 0 - if len(v) % 4: + if len(v) % 4 != 0: v = pad * (4 - (len(v) % 4)) + v for i in range(0, len(v), 4): - r = (r << 32) | struct.unpack(sf, v[i:i + 4])[0] + r = (r << 32) | struct.unpack(f, v[i:i + 4])[0] return r def read_mpint1(self): @@ -643,19 +1119,23 @@ class ReadBuf(object): v = self.read_string() if len(v) == 0: return 0 - pad, sf = (b'\xff', '>i') if ord(v[0:1]) & 0x80 else (b'\x00', '>I') - return self._parse_mpint(v, pad, sf) + pad, f = (b'\xff', '>i') if ord(v[0:1]) & 0x80 != 0 else (b'\x00', '>I') + return self._parse_mpint(v, pad, f) def read_line(self): # type: () -> text_type return self._buf.readline().rstrip().decode('utf-8', 'replace') + def reset(self): + self._buf = BytesIO() + self._len = 0 + super(ReadBuf, self).reset() class WriteBuf(object): def __init__(self, data=None): # type: (Optional[binary_type]) -> None super(WriteBuf, self).__init__() - self._wbuf = BytesIO(data) if data else BytesIO() + self._wbuf = BytesIO(data) if data is not None else BytesIO() def write(self, data): # type: (binary_type) -> WriteBuf @@ -702,7 +1182,7 @@ class WriteBuf(object): ql = (length + 7) // 8 fmt, v2 = '>{0}Q'.format(ql), [0] * ql for i in range(ql): - v2[ql - i - 1] = (n & 0xffffffffffffffff) + v2[ql - i - 1] = n & 0xffffffffffffffff n >>= 64 data = bytes(struct.pack(fmt, *v2)[-length:]) if not signed: @@ -739,20 +1219,30 @@ class WriteBuf(object): self._wbuf.seek(0) return payload + def reset(self): + self._wbuf = BytesIO() + class SSH(object): # pylint: disable=too-few-public-methods class Protocol(object): # pylint: disable=too-few-public-methods # pylint: disable=bad-whitespace SMSG_PUBLIC_KEY = 2 + MSG_DEBUG = 4 MSG_KEXINIT = 20 MSG_NEWKEYS = 21 MSG_KEXDH_INIT = 30 - MSG_KEXDH_REPLY = 32 + MSG_KEXDH_REPLY = 31 + MSG_KEXDH_GEX_REQUEST = 34 + MSG_KEXDH_GEX_GROUP = 31 + MSG_KEXDH_GEX_INIT = 32 + MSG_KEXDH_GEX_REPLY = 33 class Product(object): # pylint: disable=too-few-public-methods OpenSSH = 'OpenSSH' DropbearSSH = 'Dropbear SSH' LibSSH = 'libssh' + TinySSH = 'TinySSH' + PuTTY = 'PuTTY' class Software(object): def __init__(self, vendor, product, version, patch, os_version): @@ -798,7 +1288,7 @@ class SSH(object): # pylint: disable=too-few-public-methods else: other = str(other) mx = re.match(r'^([\d\.]+\d+)(.*)$', other) - if mx: + if bool(mx): oversion, opatch = mx.group(1), mx.group(2).strip() else: oversion, opatch = other, '' @@ -815,10 +1305,10 @@ class SSH(object): # pylint: disable=too-few-public-methods elif self.product == SSH.Product.OpenSSH: mx1 = re.match(r'^p\d(.*)', opatch) mx2 = re.match(r'^p\d(.*)', spatch) - if not (mx1 and mx2): - if mx1: + if not (bool(mx1) and bool(mx2)): + if bool(mx1): opatch = mx1.group(1) - if mx2: + if bool(mx2): spatch = mx2.group(1) if spatch < opatch: return -1 @@ -828,28 +1318,28 @@ class SSH(object): # pylint: disable=too-few-public-methods def between_versions(self, vfrom, vtill): # type: (str, str) -> bool - if vfrom and self.compare_version(vfrom) < 0: + if bool(vfrom) and self.compare_version(vfrom) < 0: return False - if vtill and self.compare_version(vtill) > 0: + if bool(vtill) and self.compare_version(vtill) > 0: return False return True def display(self, full=True): # type: (bool) -> str - r = '{0} '.format(self.vendor) if self.vendor else '' + r = '{0} '.format(self.vendor) if bool(self.vendor) else '' r += self.product - if self.version: + if bool(self.version): r += ' {0}'.format(self.version) if full: patch = self.patch or '' if self.product == SSH.Product.OpenSSH: mx = re.match(r'^(p\d)(.*)$', patch) - if mx is not None: + if bool(mx): r += mx.group(1) patch = mx.group(2).strip() - if patch: + if bool(patch): r += ' ({0})'.format(patch) - if self.os: + if bool(self.os): r += ' running on {0}'.format(self.os) return r @@ -859,16 +1349,13 @@ class SSH(object): # pylint: disable=too-few-public-methods def __repr__(self): # type: () -> str - r = 'vendor={0}'.format(self.vendor) if self.vendor else '' - if self.product: - if self.vendor: - r += ', ' - r += 'product={0}'.format(self.product) - if self.version: + r = 'vendor={0}, '.format(self.vendor) if bool(self.vendor) else '' + r += 'product={0}'.format(self.product) + if bool(self.version): r += ', version={0}'.format(self.version) - if self.patch: + if bool(self.patch): r += ', patch={0}'.format(self.patch) - if self.os: + if bool(self.os): r += ', os={0}'.format(self.os) return '<{0}({1})>'.format(self.__class__.__name__, r) @@ -887,23 +1374,23 @@ class SSH(object): # pylint: disable=too-few-public-methods @classmethod def _extract_os_version(cls, c): - # type: (Optional[str]) -> str + # type: (Optional[str]) -> Optional[str] if c is None: return None mx = re.match(r'^NetBSD(?:_Secure_Shell)?(?:[\s-]+(\d{8})(.*))?$', c) - if mx: + if bool(mx): d = cls._fix_date(mx.group(1)) return 'NetBSD' if d is None else 'NetBSD ({0})'.format(d) mx = re.match(r'^FreeBSD(?:\slocalisations)?[\s-]+(\d{8})(.*)$', c) - if not mx: + if not bool(mx): mx = re.match(r'^[^@]+@FreeBSD\.org[\s-]+(\d{8})(.*)$', c) - if mx: + if bool(mx): d = cls._fix_date(mx.group(1)) return 'FreeBSD' if d is None else 'FreeBSD ({0})'.format(d) w = ['RemotelyAnywhere', 'DesktopAuthority', 'RemoteSupportManager'] for win_soft in w: mx = re.match(r'^in ' + win_soft + r' ([\d\.]+\d)$', c) - if mx: + if bool(mx): ver = mx.group(1) return 'Microsoft Windows ({0} {1})'.format(win_soft, ver) generic = ['NetBSD', 'FreeBSD'] @@ -914,41 +1401,54 @@ class SSH(object): # pylint: disable=too-few-public-methods @classmethod def parse(cls, banner): - # type: (SSH.Banner) -> SSH.Software + # type: (SSH.Banner) -> Optional[SSH.Software] # pylint: disable=too-many-return-statements software = str(banner.software) mx = re.match(r'^dropbear_([\d\.]+\d+)(.*)', software) - if mx: + v = None # type: Optional[str] + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = 'Matt Johnston', SSH.Product.DropbearSSH v = None return cls(v, p, mx.group(1), patch, None) mx = re.match(r'^OpenSSH[_\.-]+([\d\.]+\d+)(.*)', software) - if mx: + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = 'OpenBSD', SSH.Product.OpenSSH v = None os_version = cls._extract_os_version(banner.comments) return cls(v, p, mx.group(1), patch, os_version) mx = re.match(r'^libssh-([\d\.]+\d+)(.*)', software) - if mx: + if bool(mx): + patch = cls._fix_patch(mx.group(2)) + v, p = None, SSH.Product.LibSSH + os_version = cls._extract_os_version(banner.comments) + return cls(v, p, mx.group(1), patch, os_version) + mx = re.match(r'^libssh_([\d\.]+\d+)(.*)', software) + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = None, SSH.Product.LibSSH os_version = cls._extract_os_version(banner.comments) return cls(v, p, mx.group(1), patch, os_version) mx = re.match(r'^RomSShell_([\d\.]+\d+)(.*)', software) - if mx: + if bool(mx): patch = cls._fix_patch(mx.group(2)) v, p = 'Allegro Software', 'RomSShell' return cls(v, p, mx.group(1), patch, None) mx = re.match(r'^mpSSH_([\d\.]+\d+)', software) - if mx: + if bool(mx): v, p = 'HP', 'iLO (Integrated Lights-Out) sshd' return cls(v, p, mx.group(1), None, None) mx = re.match(r'^Cisco-([\d\.]+\d+)', software) - if mx: + if bool(mx): v, p = 'Cisco', 'IOS/PIX sshd' return cls(v, p, mx.group(1), None, None) + mx = re.match(r'^tinyssh_(.*)', software) + if bool(mx): + return cls(None, SSH.Product.TinySSH, mx.group(1), None, None) + mx = re.match(r'^PuTTY_Release_(.*)', software) + if bool(mx): + return cls(None, SSH.Product.PuTTY, mx.group(1), None, None) return None class Banner(object): @@ -957,7 +1457,7 @@ class SSH(object): # pylint: disable=too-few-public-methods RX_BANNER = re.compile(r'^({0}(?:(?:-{0})*)){1}$'.format(_RXP, _RXR)) def __init__(self, protocol, software, comments, valid_ascii): - # type: (Tuple[int, int], str, str, bool) -> None + # type: (Tuple[int, int], Optional[str], Optional[str], bool) -> None self.__protocol = protocol self.__software = software self.__comments = comments @@ -970,12 +1470,12 @@ class SSH(object): # pylint: disable=too-few-public-methods @property def software(self): - # type: () -> str + # type: () -> Optional[str] return self.__software @property def comments(self): - # type: () -> str + # type: () -> Optional[str] return self.__comments @property @@ -988,7 +1488,7 @@ class SSH(object): # pylint: disable=too-few-public-methods r = 'SSH-{0}.{1}'.format(self.protocol[0], self.protocol[1]) if self.software is not None: r += '-{0}'.format(self.software) - if self.comments: + if bool(self.comments): r += ' {0}'.format(self.comments) return r @@ -996,19 +1496,19 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: () -> str p = '{0}.{1}'.format(self.protocol[0], self.protocol[1]) r = 'protocol={0}'.format(p) - if self.software: + if self.software is not None: r += ', software={0}'.format(self.software) - if self.comments: + if bool(self.comments): r += ', comments={0}'.format(self.comments) return '<{0}({1})>'.format(self.__class__.__name__, r) @classmethod def parse(cls, banner): - # type: (text_type) -> SSH.Banner - valid_ascii = utils.is_ascii(banner) - ascii_banner = utils.to_ascii(banner) + # type: (text_type) -> Optional[SSH.Banner] + valid_ascii = utils.is_print_ascii(banner) + ascii_banner = utils.to_print_ascii(banner) mx = cls.RX_BANNER.match(ascii_banner) - if mx is None: + if not bool(mx): return None protocol = min(re.findall(cls.RX_PROTOCOL, mx.group(1))) protocol = (int(protocol[0]), int(protocol[1])) @@ -1039,20 +1539,339 @@ class SSH(object): # pylint: disable=too-few-public-methods r = h.decode('ascii').rstrip('=') return u'SHA256:{0}'.format(r) + class Algorithm(object): + class Timeframe(object): + def __init__(self): + # type: () -> None + self.__storage = {} # type: Dict[str, List[Optional[str]]] + + def __contains__(self, product): + # type: (str) -> bool + return product in self.__storage + + def __getitem__(self, product): + # type: (str) -> Sequence[Optional[str]] + return tuple(self.__storage.get(product, [None]*4)) + + def __str__(self): + # type: () -> str + return self.__storage.__str__() + + def __repr__(self): + # type: () -> str + return self.__str__() + + def get_from(self, product, for_server=True): + # type: (str, bool) -> Optional[str] + return self[product][0 if bool(for_server) else 2] + + def get_till(self, product, for_server=True): + # type: (str, bool) -> Optional[str] + return self[product][1 if bool(for_server) else 3] + + def _update(self, versions, pos): + # type: (Optional[str], int) -> None + ssh_versions = {} # type: Dict[str, str] + for_srv, for_cli = pos < 2, pos > 1 + for v in (versions or '').split(','): + ssh_prod, ssh_ver, is_cli = SSH.Algorithm.get_ssh_version(v) + if (not ssh_ver or + (is_cli and for_srv) or + (not is_cli and for_cli and ssh_prod in ssh_versions)): + continue + ssh_versions[ssh_prod] = ssh_ver + for ssh_product, ssh_version in ssh_versions.items(): + if ssh_product not in self.__storage: + self.__storage[ssh_product] = [None]*4 + prev = self[ssh_product][pos] + if (prev is None or + (prev < ssh_version and pos % 2 == 0) or + (prev > ssh_version and pos % 2 == 1)): + self.__storage[ssh_product][pos] = ssh_version + + def update(self, versions, for_server=None): + # type: (List[Optional[str]], Optional[bool]) -> SSH.Algorithm.Timeframe + for_cli = for_server is None or for_server is False + for_srv = for_server is None or for_server is True + vlen = len(versions) + for i in range(min(3, vlen)): + if for_srv and i < 2: + self._update(versions[i], i) + if for_cli and (i % 2 == 0 or vlen == 2): + self._update(versions[i], 3 - 0**i) + return self + + @staticmethod + def get_ssh_version(version_desc): + # type: (str) -> Tuple[str, str, bool] + is_client = version_desc.endswith('C') + if is_client: + version_desc = version_desc[:-1] + if version_desc.startswith('d'): + return SSH.Product.DropbearSSH, version_desc[1:], is_client + elif version_desc.startswith('l1'): + return SSH.Product.LibSSH, version_desc[2:], is_client + else: + return SSH.Product.OpenSSH, version_desc, is_client + + @classmethod + def get_since_text(cls, versions): + # type: (List[Optional[str]]) -> Optional[text_type] + tv = [] + if len(versions) == 0 or versions[0] is None: + return None + for v in versions[0].split(','): + ssh_prod, ssh_ver, is_cli = cls.get_ssh_version(v) + if not ssh_ver: + continue + if ssh_prod in [SSH.Product.LibSSH]: + continue + if is_cli: + ssh_ver = '{0} (client only)'.format(ssh_ver) + tv.append('{0} {1}'.format(ssh_prod, ssh_ver)) + if len(tv) == 0: + return None + return 'available since ' + ', '.join(tv).rstrip(', ') + + class Algorithms(object): + def __init__(self, pkm, kex): + # type: (Optional[SSH1.PublicKeyMessage], Optional[SSH2.Kex]) -> None + self.__ssh1kex = pkm + self.__ssh2kex = kex + + @property + def ssh1kex(self): + # type: () -> Optional[SSH1.PublicKeyMessage] + return self.__ssh1kex + + @property + def ssh2kex(self): + # type: () -> Optional[SSH2.Kex] + return self.__ssh2kex + + @property + def ssh1(self): + # type: () -> Optional[SSH.Algorithms.Item] + if self.ssh1kex is None: + return None + item = SSH.Algorithms.Item(1, SSH1.KexDB.ALGORITHMS) + item.add('key', [u'ssh-rsa1']) + item.add('enc', self.ssh1kex.supported_ciphers) + item.add('aut', self.ssh1kex.supported_authentications) + return item + + @property + def ssh2(self): + # type: () -> Optional[SSH.Algorithms.Item] + if self.ssh2kex is None: + return None + item = SSH.Algorithms.Item(2, SSH2.KexDB.ALGORITHMS) + item.add('kex', self.ssh2kex.kex_algorithms) + item.add('key', self.ssh2kex.key_algorithms) + item.add('enc', self.ssh2kex.server.encryption) + item.add('mac', self.ssh2kex.server.mac) + return item + + @property + def values(self): + # type: () -> Iterable[SSH.Algorithms.Item] + for item in [self.ssh1, self.ssh2]: + if item is not None: + yield item + + @property + def maxlen(self): + # type: () -> int + def _ml(items): + # type: (Sequence[text_type]) -> int + return max(len(i) for i in items) + maxlen = 0 + if self.ssh1kex is not None: + maxlen = max(_ml(self.ssh1kex.supported_ciphers), + _ml(self.ssh1kex.supported_authentications), + maxlen) + if self.ssh2kex is not None: + maxlen = max(_ml(self.ssh2kex.kex_algorithms), + _ml(self.ssh2kex.key_algorithms), + _ml(self.ssh2kex.server.encryption), + _ml(self.ssh2kex.server.mac), + maxlen) + return maxlen + + def get_ssh_timeframe(self, for_server=None): + # type: (Optional[bool]) -> SSH.Algorithm.Timeframe + timeframe = SSH.Algorithm.Timeframe() + for alg_pair in self.values: + alg_db = alg_pair.db + for alg_type, alg_list in alg_pair.items(): + for alg_name in alg_list: + alg_name_native = utils.to_ntext(alg_name) + alg_desc = alg_db[alg_type].get(alg_name_native) + if alg_desc is None: + continue + versions = alg_desc[0] + timeframe.update(versions, for_server) + return timeframe + + def get_recommendations(self, software, for_server=True): + # type: (Optional[SSH.Software], bool) -> Tuple[Optional[SSH.Software], Dict[int, Dict[str, Dict[str, Dict[str, int]]]]] + # pylint: disable=too-many-locals,too-many-statements + vproducts = [SSH.Product.OpenSSH, + SSH.Product.DropbearSSH, + SSH.Product.LibSSH, + SSH.Product.TinySSH] + # Set to True if server is not one of vproducts, above. + unknown_software = False + if software is not None: + if software.product not in vproducts: + unknown_software = True +# +# The code below is commented out because it would try to guess what the server is, +# usually resulting in wild & incorrect recommendations. +# +# if software is None: +# ssh_timeframe = self.get_ssh_timeframe(for_server) +# for product in vproducts: +# if product not in ssh_timeframe: +# continue +# version = ssh_timeframe.get_from(product, for_server) +# if version is not None: +# software = SSH.Software(None, product, version, None, None) +# break + rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] + if software is None: + unknown_software = True + for alg_pair in self.values: + sshv, alg_db = alg_pair.sshv, alg_pair.db + rec[sshv] = {} + for alg_type, alg_list in alg_pair.items(): + if alg_type == 'aut': + continue + rec[sshv][alg_type] = {'add': {}, 'del': {}, 'chg': {}} + for n, alg_desc in alg_db[alg_type].items(): + versions = alg_desc[0] + if len(versions) == 0 or versions[0] is None: + continue + matches = False + if unknown_software: + matches = True + for v in versions[0].split(','): + ssh_prefix, ssh_version, is_cli = SSH.Algorithm.get_ssh_version(v) + if not ssh_version: + continue + if (software is not None) and (ssh_prefix != software.product): + continue + if is_cli and for_server: + continue + if (software is not None) and (software.compare_version(ssh_version) < 0): + continue + matches = True + break + if not matches: + continue + adl, faults = len(alg_desc), 0 + for i in range(1, 3): + if not adl > i: + continue + fc = len(alg_desc[i]) + if fc > 0: + faults += pow(10, 2 - i) * fc + if n not in alg_list: + if faults > 0 or (alg_type == 'key' and '-cert-' in n): + continue + rec[sshv][alg_type]['add'][n] = 0 + else: + if faults == 0: + continue + if n in ['diffie-hellman-group-exchange-sha256', 'ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa-cert-v01@openssh.com']: + rec[sshv][alg_type]['chg'][n] = faults + else: + rec[sshv][alg_type]['del'][n] = faults + # If we are working with unknown software, drop all add recommendations, because we don't know if they're valid. + if unknown_software: + rec[sshv][alg_type]['add'] = {} + add_count = len(rec[sshv][alg_type]['add']) + del_count = len(rec[sshv][alg_type]['del']) + chg_count = len(rec[sshv][alg_type]['chg']) + new_alg_count = len(alg_list) + add_count - del_count + if new_alg_count < 1 and del_count > 0: + mf = min(rec[sshv][alg_type]['del'].values()) + new_del = {} + for k, cf in rec[sshv][alg_type]['del'].items(): + if cf != mf: + new_del[k] = cf + if del_count != len(new_del): + rec[sshv][alg_type]['del'] = new_del + new_alg_count += del_count - len(new_del) + if new_alg_count < 1: + del rec[sshv][alg_type] + else: + if add_count == 0: + del rec[sshv][alg_type]['add'] + if del_count == 0: + del rec[sshv][alg_type]['del'] + if chg_count == 0: + del rec[sshv][alg_type]['chg'] + if len(rec[sshv][alg_type]) == 0: + del rec[sshv][alg_type] + if len(rec[sshv]) == 0: + del rec[sshv] + return software, rec + + class Item(object): + def __init__(self, sshv, db): + # type: (int, Dict[str, Dict[str, List[List[Optional[str]]]]]) -> None + self.__sshv = sshv + self.__db = db + self.__storage = {} # type: Dict[str, List[text_type]] + + @property + def sshv(self): + # type: () -> int + return self.__sshv + + @property + def db(self): + # type: () -> Dict[str, Dict[str, List[List[Optional[str]]]]] + return self.__db + + def add(self, key, value): + # type: (str, List[text_type]) -> None + self.__storage[key] = value + + def items(self): + # type: () -> Iterable[Tuple[str, List[text_type]]] + return self.__storage.items() + class Security(object): # pylint: disable=too-few-public-methods + # Format: [starting_vuln_version, last_vuln_version, affected, CVE_ID, CVSSv2, description] + # affected: 1 = server, 2 = client, 4 = local + # Example: if it affects servers, both remote & local, then affected + # = 1. If it affects servers, but is a local issue only, + # then affected = 1 + 4 = 5. # pylint: disable=bad-whitespace CVE = { 'Dropbear SSH': [ + ['0.0', '2018.76', 1, 'CVE-2018-15599', 5.0, 'remote users may enumerate users on the system'], + ['0.0', '2017.74', 5, 'CVE-2017-9079', 4.7, 'local users can read certain files as root'], + ['0.0', '2017.74', 5, 'CVE-2017-9078', 9.3, 'local users may elevate privileges to root under certain conditions'], + ['0.0', '2016.73', 5, 'CVE-2016-7409', 2.1, 'local users can read process memory under limited conditions'], + ['0.0', '2016.73', 1, 'CVE-2016-7408', 6.5, 'remote users can execute arbitrary code'], + ['0.0', '2016.73', 5, 'CVE-2016-7407', 10.0, 'local users can execute arbitrary code'], + ['0.0', '2016.73', 1, 'CVE-2016-7406', 10.0, 'remote users can execute arbitrary code'], ['0.44', '2015.71', 1, 'CVE-2016-3116', 5.5, 'bypass command restrictions via xauth command injection'], ['0.28', '2013.58', 1, 'CVE-2013-4434', 5.0, 'discover valid usernames through different time delays'], - ['0.28', '2013.58', 1, 'CVE-2013-4421', 5.0, 'cause DoS (memory consumption) via a compressed packet'], + ['0.28', '2013.58', 1, 'CVE-2013-4421', 5.0, 'cause DoS via a compressed packet (memory consumption)'], ['0.52', '2011.54', 1, 'CVE-2012-0920', 7.1, 'execute arbitrary code or bypass command restrictions'], ['0.40', '0.48.1', 1, 'CVE-2007-1099', 7.5, 'conduct a MitM attack (no warning for hostkey mismatch)'], - ['0.28', '0.47', 1, 'CVE-2006-1206', 7.5, 'cause DoS (slot exhaustion) via large number of connections'], + ['0.28', '0.47', 1, 'CVE-2006-1206', 7.5, 'cause DoS via large number of connections (slot exhaustion)'], ['0.39', '0.47', 1, 'CVE-2006-0225', 4.6, 'execute arbitrary commands via scp with crafted filenames'], ['0.28', '0.46', 1, 'CVE-2005-4178', 6.5, 'execute arbitrary code via buffer overflow vulnerability'], ['0.28', '0.42', 1, 'CVE-2004-2486', 7.5, 'execute arbitrary code via DSS verification code']], 'libssh': [ + ['0.6.4', '0.6.4', 1, 'CVE-2018-10933', 6.4, 'authentication bypass'], + ['0.7.0', '0.7.5', 1, 'CVE-2018-10933', 6.4, 'authentication bypass'], + ['0.8.0', '0.8.3', 1, 'CVE-2018-10933', 6.4, 'authentication bypass'], ['0.1', '0.7.2', 1, 'CVE-2016-0739', 4.3, 'conduct a MitM attack (weakness in DH key generation)'], ['0.5.1', '0.6.4', 1, 'CVE-2015-3146', 5.0, 'cause DoS via kex packets (null pointer dereference)'], ['0.5.1', '0.6.3', 1, 'CVE-2014-8132', 5.0, 'cause DoS via kex init packet (dangling pointer)'], @@ -1062,7 +1881,77 @@ class SSH(object): # pylint: disable=too-few-public-methods ['0.4.7', '0.5.2', 1, 'CVE-2012-4562', 7.5, 'cause DoS or execute arbitrary code (overflow check)'], ['0.4.7', '0.5.2', 1, 'CVE-2012-4561', 5.0, 'cause DoS via unspecified vectors (invalid pointer)'], ['0.4.7', '0.5.2', 1, 'CVE-2012-4560', 7.5, 'cause DoS or execute arbitrary code (buffer overflow)'], - ['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']] + ['0.4.7', '0.5.2', 1, 'CVE-2012-4559', 6.8, 'cause DoS or execute arbitrary code (double free)']], + 'OpenSSH': [ + ['7.2', '7.2p2', 1, 'CVE-2016-6515', 7.8, 'cause DoS via long password string (crypt CPU consumption)'], + ['1.2.2', '7.2', 1, 'CVE-2016-3115', 5.5, 'bypass command restrictions via crafted X11 forwarding data'], + ['5.4', '7.1', 1, 'CVE-2016-1907', 5.0, 'cause DoS via crafted network traffic (out of bounds read)'], + ['5.4', '7.1p1', 2, 'CVE-2016-0778', 4.6, 'cause DoS via requesting many forwardings (heap based buffer overflow)'], + ['5.0', '7.1p1', 2, 'CVE-2016-0777', 4.0, 'leak data via allowing transfer of entire buffer'], + ['6.0', '7.2p2', 5, 'CVE-2015-8325', 7.2, 'privilege escalation via triggering crafted environment'], + ['6.8', '6.9', 5, 'CVE-2015-6565', 7.2, 'cause DoS via writing to a device (terminal disruption)'], + ['5.0', '6.9', 5, 'CVE-2015-6564', 6.9, 'privilege escalation via leveraging sshd uid'], + ['5.0', '6.9', 5, 'CVE-2015-6563', 1.9, 'conduct impersonation attack'], + ['6.9p1', '6.9p1', 1, 'CVE-2015-5600', 8.5, 'cause Dos or aid in conduct brute force attack (CPU consumption)'], + ['6.0', '6.6', 1, 'CVE-2015-5352', 4.3, 'bypass access restrictions via a specific connection'], + ['6.0', '6.6', 2, 'CVE-2014-2653', 5.8, 'bypass SSHFP DNS RR check via unacceptable host certificate'], + ['5.0', '6.5', 1, 'CVE-2014-2532', 5.8, 'bypass environment restrictions via specific string before wildcard'], + ['1.2', '6.4', 1, 'CVE-2014-1692', 7.5, 'cause DoS via triggering error condition (memory corruption)'], + ['6.2', '6.3', 1, 'CVE-2013-4548', 6.0, 'bypass command restrictions via crafted packet data'], + ['1.2', '5.6', 1, 'CVE-2012-0814', 3.5, 'leak data via debug messages'], + ['1.2', '5.8', 1, 'CVE-2011-5000', 3.5, 'cause DoS via large value in certain length field (memory consumption)'], + ['5.6', '5.7', 2, 'CVE-2011-0539', 5.0, 'leak data or conduct hash collision attack'], + ['1.2', '6.1', 1, 'CVE-2010-5107', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], + ['1.2', '5.8', 1, 'CVE-2010-4755', 4.0, 'cause DoS via crafted glob expression (CPU and memory consumption)'], + ['1.2', '5.6', 1, 'CVE-2010-4478', 7.5, 'bypass authentication check via crafted values'], + ['4.3', '4.8', 1, 'CVE-2009-2904', 6.9, 'privilege escalation via hard links to setuid programs'], + ['4.0', '5.1', 1, 'CVE-2008-5161', 2.6, 'recover plaintext data from ciphertext'], + ['1.2', '4.6', 1, 'CVE-2008-4109', 5.0, 'cause DoS via multiple login attempts (slot exhaustion)'], + ['1.2', '4.8', 1, 'CVE-2008-1657', 6.5, 'bypass command restrictions via modifying session file'], + ['1.2.2', '4.9', 1, 'CVE-2008-1483', 6.9, 'hijack forwarded X11 connections'], + ['4.0', '4.6', 1, 'CVE-2007-4752', 7.5, 'privilege escalation via causing an X client to be trusted'], + ['4.3p2', '4.3p2', 1, 'CVE-2007-3102', 4.3, 'allow attacker to write random data to audit log'], + ['1.2', '4.6', 1, 'CVE-2007-2243', 5.0, 'discover valid usernames through different responses'], + ['4.4', '4.4', 1, 'CVE-2006-5794', 7.5, 'bypass authentication'], + ['4.1', '4.1p1', 1, 'CVE-2006-5229', 2.6, 'discover valid usernames through different time delays'], + ['1.2', '4.3p2', 1, 'CVE-2006-5052', 5.0, 'discover valid usernames through different responses'], + ['1.2', '4.3p2', 1, 'CVE-2006-5051', 9.3, 'cause DoS or execute arbitrary code (double free)'], + ['4.5', '4.5', 1, 'CVE-2006-4925', 5.0, 'cause DoS via invalid protocol sequence (crash)'], + ['1.2', '4.3p2', 1, 'CVE-2006-4924', 7.8, 'cause DoS via crafted packet (CPU consumption)'], + ['3.8.1p1', '3.8.1p1', 1, 'CVE-2006-0883', 5.0, 'cause DoS via connecting multiple times (client connection refusal)'], + ['3.0', '4.2p1', 1, 'CVE-2006-0225', 4.6, 'execute arbitrary code'], + ['2.1', '4.1p1', 1, 'CVE-2005-2798', 5.0, 'leak data about authentication credentials'], + ['3.5', '3.5p1', 1, 'CVE-2004-2760', 6.8, 'leak data through different connection states'], + ['2.3', '3.7.1p2', 1, 'CVE-2004-2069', 5.0, 'cause DoS via large number of connections (slot exhaustion)'], + ['3.0', '3.4p1', 1, 'CVE-2004-0175', 4.3, 'leak data through directoy traversal'], + ['1.2', '3.9p1', 1, 'CVE-2003-1562', 7.6, 'leak data about authentication credentials'], + ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0787', 7.5, 'privilege escalation via modifying stack'], + ['3.1p1', '3.7.1p1', 1, 'CVE-2003-0786', 10.0, 'privilege escalation via bypassing authentication'], + ['1.0', '3.7.1', 1, 'CVE-2003-0695', 7.5, 'cause DoS or execute arbitrary code'], + ['1.0', '3.7', 1, 'CVE-2003-0693', 10.0, 'execute arbitrary code'], + ['3.0', '3.6.1p2', 1, 'CVE-2003-0386', 7.5, 'bypass address restrictions for connection'], + ['3.1p1', '3.6.1p1', 1, 'CVE-2003-0190', 5.0, 'discover valid usernames through different time delays'], + ['3.2.2', '3.2.2', 1, 'CVE-2002-0765', 7.5, 'bypass authentication'], + ['1.2.2', '3.3p1', 1, 'CVE-2002-0640', 10.0, 'execute arbitrary code'], + ['1.2.2', '3.3p1', 1, 'CVE-2002-0639', 10.0, 'execute arbitrary code'], + ['2.1', '3.2', 1, 'CVE-2002-0575', 7.5, 'privilege escalation'], + ['2.1', '3.0.2p1', 2, 'CVE-2002-0083', 10.0, 'privilege escalation'], + ['3.0', '3.0p1', 1, 'CVE-2001-1507', 7.5, 'bypass authentication'], + ['1.2.3', '3.0.1p1', 5, 'CVE-2001-0872', 7.2, 'privilege escalation via crafted environment variables'], + ['1.2.3', '2.1.1', 1, 'CVE-2001-0361', 4.0, 'recover plaintext from ciphertext'], + ['1.2', '2.1', 1, 'CVE-2000-0525', 10.0, 'execute arbitrary code (improper privileges)']], + 'PuTTY': [ + ['0.0', '0.71', 2, 'CVE-2019-XXXX', 5.0, 'undefined vulnerability in obsolete SSHv1 protocol handling'], + ['0.0', '0.71', 6, 'CVE-2019-XXXX', 5.0, 'local privilege escalation in Pageant'], + ['0.0', '0.70', 2, 'CVE-2019-9898', 7.5, 'potential recycling of random numbers'], + ['0.0', '0.70', 2, 'CVE-2019-9897', 5.0, 'multiple denial-of-service issues from writing to the terminal'], + ['0.0', '0.70', 6, 'CVE-2019-9896', 4.6, 'local application hijacking through malicious Windows help file'], + ['0.0', '0.70', 2, 'CVE-2019-9894', 6.4, 'buffer overflow in RSA key exchange'], + ['0.0', '0.69', 6, 'CVE-2016-6167', 4.4, 'local application hijacking through untrusted DLL loading'], + ['0.0', '0.67', 2, 'CVE-2017-6542', 7.5, 'buffer overflow in UNIX client that can result in privilege escalation or denial-of-service'], + ['0.0', '0.66', 2, 'CVE-2016-2563', 7.5, 'buffer overflow in SCP command-line utility'], + ['0.0', '0.65', 2, 'CVE-2015-5309', 4.3, 'integer overflow in terminal-handling code'], + ] } # type: Dict[str, List[List[Any]]] TXT = { 'Dropbear SSH': [ @@ -1079,29 +1968,37 @@ class SSH(object): # pylint: disable=too-few-public-methods SM_BANNER_SENT = 1 - def __init__(self, host, port): - # type: (str, int) -> None + def __init__(self, host, port, ipvo, timeout): + # type: (Optional[str], int) -> None super(SSH.Socket, self).__init__() + self.__sock = None # type: Optional[socket.socket] + self.__sock_map = {} self.__block_size = 8 self.__state = 0 self.__header = [] # type: List[text_type] self.__banner = None # type: Optional[SSH.Banner] +# if host is None: +# raise ValueError('undefined host') + nport = utils.parse_int(port) + if nport < 1 or nport > 65535: + raise ValueError('invalid port: {0}'.format(port)) self.__host = host - self.__port = port - self.__sock = None # type: socket.socket - - def __enter__(self): - # type: () -> SSH.Socket - return self + self.__port = nport + if ipvo is not None: + self.__ipvo = ipvo + else: + self.__ipvo = () + self.__timeout = timeout + def _resolve(self, ipvo): # type: (Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]] - ipvo = tuple(filter(lambda x: x in (4, 6), utils.unique_seq(ipvo))) + ipvo = tuple([x for x in utils.unique_seq(ipvo) if x in (4, 6)]) ipvo_len = len(ipvo) prefer_ipvo = ipvo_len > 0 prefer_ipv4 = prefer_ipvo and ipvo[0] == 4 - if len(ipvo) == 1: - family = {4: socket.AF_INET, 6: socket.AF_INET6}.get(ipvo[0]) + if ipvo_len == 1: + family = socket.AF_INET if ipvo[0] == 4 else socket.AF_INET6 else: family = socket.AF_UNSPEC try: @@ -1110,23 +2007,66 @@ class SSH(object): # pylint: disable=too-few-public-methods if prefer_ipvo: r = sorted(r, key=lambda x: x[0], reverse=not prefer_ipv4) check = any(stype == rline[2] for rline in r) - for (af, socktype, proto, canonname, addr) in r: + for af, socktype, _proto, _canonname, addr in r: if not check or socktype == socket.SOCK_STREAM: - yield (af, addr) + yield af, addr except socket.error as e: out.fail('[exception] {0}'.format(e)) sys.exit(1) - - def connect(self, ipvo=(), cto=3.0, rto=5.0): - # type: (Sequence[int], float, float) -> None + + + # Listens on a server socket and accepts one connection (used for + # auditing client connections). + def listen_and_accept(self): + + try: + # Socket to listen on all IPv4 addresses. + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('0.0.0.0', self.__port)) + s.listen() + self.__sock_map[s.fileno()] = s + except Exception as e: + print("Warning: failed to listen on any IPv4 interfaces.") + pass + + try: + # Socket to listen on all IPv6 addresses. + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + s.bind(('::', self.__port)) + s.listen() + self.__sock_map[s.fileno()] = s + except Exception as e: + print("Warning: failed to listen on any IPv6 interfaces.") + pass + + # If we failed to listen on any interfaces, terminate. + if len(self.__sock_map.keys()) == 0: + print("Error: failed to listen on any IPv4 and IPv6 interfaces!") + exit(-1) + + # Wait for a connection on either socket. + fds = select.select(self.__sock_map.keys(), [], []) + + # Accept the connection. + c, addr = self.__sock_map[fds[0][0]].accept() + self.client_host = addr[0] + self.client_port = addr[1] + c.settimeout(self.__timeout) + self.__sock = c + + + def connect(self): + # type: () -> None err = None - for (af, addr) in self._resolve(ipvo): + for af, addr in self._resolve(self.__ipvo): s = None try: s = socket.socket(af, socket.SOCK_STREAM) - s.settimeout(cto) + s.settimeout(self.__timeout) s.connect(addr) - s.settimeout(rto) self.__sock = s return except socket.error as e: @@ -1141,16 +2081,19 @@ class SSH(object): # pylint: disable=too-few-public-methods sys.exit(1) def get_banner(self, sshv=2): - # type: (int) -> Tuple[Optional[SSH.Banner], List[text_type]] - banner = 'SSH-{0}-OpenSSH_7.3'.format('1.5' if sshv == 1 else '2.0') - rto = self.__sock.gettimeout() - self.__sock.settimeout(0.7) - s, e = self.recv() - self.__sock.settimeout(rto) - if s < 0: - return self.__banner, self.__header + # type: (int) -> Tuple[Optional[SSH.Banner], List[text_type], Optional[str]] + if self.__sock is None: + return self.__banner, self.__header, 'not connected' + banner = SSH_HEADER.format('1.5' if sshv == 1 else '2.0') if self.__state < self.SM_BANNER_SENT: self.send_banner(banner) +# rto = self.__sock.gettimeout() +# self.__sock.settimeout(0.7) + s, e = self.recv() +# self.__sock.settimeout(rto) + if s < 0: + return self.__banner, self.__header, e + e = None while self.__banner is None: if not s > 0: s, e = self.recv() @@ -1166,34 +2109,38 @@ class SSH(object): # pylint: disable=too-few-public-methods continue self.__header.append(line) s = 0 - return self.__banner, self.__header + return self.__banner, self.__header, e def recv(self, size=2048): # type: (int) -> Tuple[int, Optional[str]] + if self.__sock is None: + return -1, 'not connected' try: data = self.__sock.recv(size) except socket.timeout: - return (-1, 'timeout') + return -1, 'timed out' except socket.error as e: if e.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK): - return (0, 'retry') - return (-1, str(e.args[-1])) + return 0, 'retry' + return -1, str(e.args[-1]) if len(data) == 0: - return (-1, None) + return -1, None pos = self._buf.tell() self._buf.seek(0, 2) self._buf.write(data) self._len += len(data) self._buf.seek(pos, 0) - return (len(data), None) + return len(data), None def send(self, data): # type: (binary_type) -> Tuple[int, Optional[str]] + if self.__sock is None: + return -1, 'not connected' try: self.__sock.send(data) - return (0, None) + return 0, None except socket.error as e: - return (-1, str(e.args[-1])) + return -1, str(e.args[-1]) self.__sock.send(data) def send_banner(self, banner): @@ -1218,7 +2165,7 @@ class SSH(object): # pylint: disable=too-few-public-methods header.write_int(packet_length) # XXX: validate length if sshv == 1: - padding_length = (8 - packet_length % 8) + padding_length = 8 - packet_length % 8 self.ensure_read(padding_length) padding = self.read(padding_length) header.write(padding) @@ -1259,7 +2206,7 @@ class SSH(object): # pylint: disable=too-few-public-methods e = header.write_flush().strip() else: e = ex.args[0].encode('utf-8') - return (-1, e) + return -1, e def send_packet(self): # type: () -> Tuple[int, Optional[str]] @@ -1271,13 +2218,27 @@ class SSH(object): # pylint: disable=too-few-public-methods pad_bytes = b'\x00' * padding data = struct.pack('>Ib', plen, padding) + payload + pad_bytes return self.send(data) - + + # Returns True if this Socket is connected, otherwise False. + def is_connected(self): + return (self.__sock is not None) + + def close(self): + self.__cleanup() + self.reset() + self.__state = 0 + self.__header = [] + self.__banner = None + + def reset(self): + super(SSH.Socket, self).reset() + def _close_socket(self, s): # type: (Optional[socket.socket]) -> None try: if s is not None: s.shutdown(socket.SHUT_RDWR) - s.close() + s.close() # pragma: nocover except: # pylint: disable=bare-except pass @@ -1285,36 +2246,205 @@ class SSH(object): # pylint: disable=too-few-public-methods # type: () -> None self.__cleanup() - def __exit__(self, *args): - # type: (*Any) -> None - self.__cleanup() - def __cleanup(self): # type: () -> None self._close_socket(self.__sock) + for fd in self.__sock_map: + self._close_socket(self.__sock_map[fd]) + self.__sock = None -class KexDH(object): - def __init__(self, alg, g, p): +class KexDH(object): # pragma: nocover + def __init__(self, kex_name, hash_alg, g, p): # type: (str, int, int) -> None - self.__alg = alg + self.__kex_name = kex_name + self.__hash_alg = hash_alg + self.__g = 0 + self.__p = 0 + self.__q = 0 + self.__x = 0 + self.__e = 0 + self.set_params(g, p) + + self.__ed25519_pubkey = 0 + self.__hostkey_type = None + self.__hostkey_e = 0 + self.__hostkey_n = 0 + self.__hostkey_n_len = 0 # Length of the host key modulus. + self.__ca_n_len = 0 # Length of the CA key modulus (if hostkey is a cert). + self.__f = 0 + self.__h_sig = 0 + + + def set_params(self, g, p): self.__g = g self.__p = p self.__q = (self.__p - 1) // 2 - self.__x = None # type: Optional[int] - self.__e = None # type: Optional[int] - - def send_init(self, s): + self.__x = 0 + self.__e = 0 + + + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): # type: (SSH.Socket) -> None r = random.SystemRandom() self.__x = r.randrange(2, self.__q) self.__e = pow(self.__g, self.__x, self.__p) - s.write_byte(SSH.Protocol.MSG_KEXDH_INIT) + s.write_byte(init_msg) s.write_mpint2(self.__e) s.send_packet() + # Parse a KEXDH_REPLY or KEXDH_GEX_REPLY message from the server. This + # contains the host key, among other things. Function returns the host + # key blob (from which the fingerprint can be calculated). + def recv_reply(self, s, parse_host_key_size=True): + packet_type, payload = s.read_packet(2) -class KexGroup1(KexDH): + # Skip any & all MSG_DEBUG messages. + while packet_type == SSH.Protocol.MSG_DEBUG: + packet_type, payload = s.read_packet(2) + + if packet_type != -1 and packet_type not in [SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY]: + # TODO: change Exception to something more specific. + raise Exception('Expected MSG_KEXDH_REPLY (%d) or MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (SSH.Protocol.MSG_KEXDH_REPLY, SSH.Protocol.MSG_KEXDH_GEX_REPLY, packet_type)) + elif packet_type == -1: + # A connection error occurred. We can't parse anything, so just + # return. The host key modulus (and perhaps certificate modulus) + # will remain at length 0. + return None + + hostkey_len = f_len = h_sig_len = 0 # pylint: disable=unused-variable + hostkey_type_len = hostkey_e_len = 0 # pylint: disable=unused-variable + key_id_len = principles_len = 0 # pylint: disable=unused-variable + critical_options_len = extensions_len = 0 # pylint: disable=unused-variable + nonce_len = ca_key_len = ca_key_type_len = 0 # pylint: disable=unused-variable + ca_key_len = ca_key_type_len = ca_key_e_len = 0 # pylint: disable=unused-variable + + key_id = principles = None # pylint: disable=unused-variable + critical_options = extensions = None # pylint: disable=unused-variable + valid_after = valid_before = None # pylint: disable=unused-variable + nonce = ca_key = ca_key_type = None # pylint: disable=unused-variable + ca_key_e = ca_key_n = None # pylint: disable=unused-variable + + # Get the host key blob, F, and signature. + ptr = 0 + hostkey, hostkey_len, ptr = KexDH.__get_bytes(payload, ptr) + + # If we are not supposed to parse the host key size (i.e.: it is a type that is of fixed size such as ed25519), then stop here. + if not parse_host_key_size: + return hostkey + + self.__f, f_len, ptr = KexDH.__get_bytes(payload, ptr) + self.__h_sig, h_sig_len, ptr = KexDH.__get_bytes(payload, ptr) + + # Now pick apart the host key blob. + # Get the host key type (i.e.: 'ssh-rsa', 'ssh-ed25519', etc). + ptr = 0 + self.__hostkey_type, hostkey_type_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # If this is an RSA certificate, skip over the nonce. + if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'): + nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # The public key exponent. + hostkey_e, hostkey_e_len, ptr = KexDH.__get_bytes(hostkey, ptr) + self.__hostkey_e = int(binascii.hexlify(hostkey_e), 16) + + # Here is the modulus size & actual modulus of the host key public key. + hostkey_n, self.__hostkey_n_len, ptr = KexDH.__get_bytes(hostkey, ptr) + self.__hostkey_n = int(binascii.hexlify(hostkey_n), 16) + + # If this is an RSA certificate, continue parsing to extract the CA + # key. + if self.__hostkey_type.startswith(b'ssh-rsa-cert-v0'): + # Skip over the serial number. + ptr += 8 + + # Get the certificate type. + cert_type = int(binascii.hexlify(hostkey[ptr:ptr + 4]), 16) + ptr += 4 + + # Only SSH2_CERT_TYPE_HOST (2) makes sense in this context. + if cert_type == 2: + + # Skip the key ID (this is the serial number of the + # certificate). + key_id, key_id_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # The principles, which are... I don't know what. + principles, principles_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # The timestamp that this certificate is valid after. + valid_after = hostkey[ptr:ptr + 8] + ptr += 8 + + # The timestamp that this certificate is valid before. + valid_before = hostkey[ptr:ptr + 8] + ptr += 8 + + # TODO: validate the principles, and time range. + + # The critical options. + critical_options, critical_options_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # Certificate extensions. + extensions, extensions_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # Another nonce. + nonce, nonce_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # Finally, we get to the CA key. + ca_key, ca_key_len, ptr = KexDH.__get_bytes(hostkey, ptr) + + # Last in the host key blob is the CA signature. It isn't + # interesting to us, so we won't bother parsing any further. + # The CA key has the modulus, however... + ptr = 0 + + # 'ssh-rsa', 'rsa-sha2-256', etc. + ca_key_type, ca_key_type_len, ptr = KexDH.__get_bytes(ca_key, ptr) + + # CA's public key exponent. + ca_key_e, ca_key_e_len, ptr = KexDH.__get_bytes(ca_key, ptr) + + # CA's modulus. Bingo. + ca_key_n, self.__ca_n_len, ptr = KexDH.__get_bytes(ca_key, ptr) + + return hostkey + + @staticmethod + def __get_bytes(buf, ptr): + num_bytes = struct.unpack('>I', buf[ptr:ptr + 4])[0] + ptr += 4 + return buf[ptr:ptr + num_bytes], num_bytes, ptr + num_bytes + + # Converts a modulus length in bytes to its size in bits, after some + # possible adjustments. + @staticmethod + def __adjust_key_size(size): + size = size * 8 + # Actual keys are observed to be about 8 bits bigger than expected + # (i.e.: 1024-bit keys have a 1032-bit modulus). Check if this is + # the case, and subtract 8 if so. This simply improves readability + # in the UI. + if (size >> 3) % 2 != 0: + size = size - 8 + return size + + # Returns the size of the hostkey, in bits. + def get_hostkey_size(self): + return KexDH.__adjust_key_size(self.__hostkey_n_len) + + # Returns the size of the CA key, in bits. + def get_ca_size(self): + return KexDH.__adjust_key_size(self.__ca_n_len) + + # Returns the size of the DH modulus, in bits. + def get_dh_modulus_size(self): + # -2 to account for the '0b' prefix in the string. + return len(bin(self.__p)) - 2 + + +class KexGroup1(KexDH): # pragma: nocover def __init__(self): # type: () -> None # rfc2409: second oakley group @@ -1323,11 +2453,11 @@ class KexGroup1(KexDH): 'f25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff' '5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381' 'ffffffffffffffff', 16) - super(KexGroup1, self).__init__('sha1', 2, p) + super(KexGroup1, self).__init__('KexGroup1', 'sha1', 2, p) -class KexGroup14(KexDH): - def __init__(self): +class KexGroup14(KexDH): # pragma: nocover + def __init__(self, hash_alg): # type: () -> None # rfc3526: 2048-bit modp group p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67' @@ -1339,334 +2469,225 @@ class KexGroup14(KexDH): 'ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c5' '5df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa0510' '15728e5a8aacaa68ffffffffffffffff', 16) - super(KexGroup14, self).__init__('sha1', 2, p) + super(KexGroup14, self).__init__('KexGroup14', hash_alg, 2, p) -class KexDB(object): # pylint: disable=too-few-public-methods - # pylint: disable=bad-whitespace - WARN_OPENSSH72_LEGACY = 'disabled (in client) since OpenSSH 7.2, legacy algorithm' - FAIL_OPENSSH70_LEGACY = 'removed since OpenSSH 7.0, legacy algorithm' - FAIL_OPENSSH70_WEAK = 'removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm' - FAIL_OPENSSH70_LOGJAM = 'disabled (in client) since OpenSSH 7.0, logjam attack' - INFO_OPENSSH69_CHACHA = 'default cipher since OpenSSH 6.9.' - FAIL_OPENSSH67_UNSAFE = 'removed (in server) since OpenSSH 6.7, unsafe algorithm' - FAIL_OPENSSH61_REMOVE = 'removed since OpenSSH 6.1, removed from specification' - FAIL_OPENSSH31_REMOVE = 'removed since OpenSSH 3.1' - FAIL_DBEAR67_DISABLED = 'disabled since Dropbear SSH 2015.67' - FAIL_DBEAR53_DISABLED = 'disabled since Dropbear SSH 0.53' - FAIL_PLAINTEXT = 'no encryption/integrity' - WARN_CURVES_WEAK = 'using weak elliptic curves' - WARN_RNDSIG_KEY = 'using weak random number generator could reveal the key' - WARN_MODULUS_SIZE = 'using small 1024-bit modulus' - WARN_MODULUS_CUSTOM = 'using custom size modulus (possibly weak)' - WARN_HASH_WEAK = 'using weak hashing algorithm' - WARN_CIPHER_MODE = 'using weak cipher mode' - WARN_BLOCK_SIZE = 'using small 64-bit block size' - WARN_CIPHER_WEAK = 'using weak cipher' - WARN_ENCRYPT_AND_MAC = 'using encrypt-and-MAC mode' - WARN_TAG_SIZE = 'using small 64-bit tag size' - - ALGORITHMS = { - 'kex': { - 'diffie-hellman-group1-sha1': [['2.3.0,d0.28,l10.2', '6.6', '6.9'], [FAIL_OPENSSH67_UNSAFE, FAIL_OPENSSH70_LOGJAM], [WARN_MODULUS_SIZE, WARN_HASH_WEAK]], - 'diffie-hellman-group14-sha1': [['3.9,d0.53,l10.6.0'], [], [WARN_HASH_WEAK]], - 'diffie-hellman-group14-sha256': [['7.3,d2016.73']], - 'diffie-hellman-group16-sha512': [['7.3,d2016.73']], - 'diffie-hellman-group18-sha512': [['7.3']], - 'diffie-hellman-group-exchange-sha1': [['2.3.0', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], - 'diffie-hellman-group-exchange-sha256': [['4.4'], [], [WARN_MODULUS_CUSTOM]], - 'ecdh-sha2-nistp256': [['5.7,d2013.62,l10.6.0'], [WARN_CURVES_WEAK]], - 'ecdh-sha2-nistp384': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], - 'ecdh-sha2-nistp521': [['5.7,d2013.62'], [WARN_CURVES_WEAK]], - 'curve25519-sha256@libssh.org': [['6.5,d2013.62,l10.6.0']], - 'kexguess2@matt.ucc.asn.au': [['d2013.57']], - }, - 'key': { - 'rsa-sha2-256': [['7.2']], - 'rsa-sha2-512': [['7.2']], - 'ssh-ed25519': [['6.5,l10.7.0']], - 'ssh-ed25519-cert-v01@openssh.com': [['6.5']], - 'ssh-rsa': [['2.5.0,d0.28,l10.2']], - 'ssh-dss': [['2.1.0,d0.28,l10.2', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp256': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp384': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp521': [['5.7,d2013.62,l10.6.4'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], []], - 'ssh-dss-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_OPENSSH70_LEGACY], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], - 'ssh-rsa-cert-v01@openssh.com': [['5.6']], - 'ssh-dss-cert-v01@openssh.com': [['5.6', '6.9'], [FAIL_OPENSSH70_WEAK], [WARN_MODULUS_SIZE, WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp256-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp384-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - 'ecdsa-sha2-nistp521-cert-v01@openssh.com': [['5.7'], [WARN_CURVES_WEAK], [WARN_RNDSIG_KEY]], - }, - 'enc': { - 'none': [['1.2.2,d2013.56,l10.2'], [FAIL_PLAINTEXT]], - '3des-cbc': [['1.2.2,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_WEAK, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], - '3des-ctr': [['d0.52']], - 'blowfish-cbc': [['1.2.2,d0.28,l10.2', '6.6,d0.52', '7.1,d0.52'], [FAIL_OPENSSH67_UNSAFE, FAIL_DBEAR53_DISABLED], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], - 'twofish-cbc': [['d0.28', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], - 'twofish128-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], - 'twofish256-cbc': [['d0.47', 'd2014.66'], [FAIL_DBEAR67_DISABLED], [WARN_CIPHER_MODE]], - 'twofish128-ctr': [['d2015.68']], - 'twofish256-ctr': [['d2015.68']], - 'cast128-cbc': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE, WARN_BLOCK_SIZE]], - 'arcfour': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], - 'arcfour128': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], - 'arcfour256': [['4.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_WEAK]], - 'aes128-cbc': [['2.3.0,d0.28,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], - 'aes192-cbc': [['2.3.0,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], - 'aes256-cbc': [['2.3.0,d0.47,l10.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_CIPHER_MODE]], - 'rijndael128-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], - 'rijndael192-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], - 'rijndael256-cbc': [['2.3.0', '3.0.2'], [FAIL_OPENSSH31_REMOVE], [WARN_CIPHER_MODE]], - 'rijndael-cbc@lysator.liu.se': [['2.3.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_CIPHER_MODE]], - 'aes128-ctr': [['3.7,d0.52,l10.4.1']], - 'aes192-ctr': [['3.7,l10.4.1']], - 'aes256-ctr': [['3.7,d0.52,l10.4.1']], - 'aes128-gcm@openssh.com': [['6.2']], - 'aes256-gcm@openssh.com': [['6.2']], - 'chacha20-poly1305@openssh.com': [['6.5'], [], [], [INFO_OPENSSH69_CHACHA]], - }, - 'mac': { - 'none': [['d2013.56'], [FAIL_PLAINTEXT]], - 'hmac-sha1': [['2.1.0,d0.28,l10.2'], [], [WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-sha1-96': [['2.5.0,d0.47', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-sha2-256': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha2-256-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha2-512': [['5.9,d2013.56,l10.7.0'], [], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha2-512-96': [['5.9', '6.0'], [FAIL_OPENSSH61_REMOVE], [WARN_ENCRYPT_AND_MAC]], - 'hmac-md5': [['2.1.0,d0.28', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-md5-96': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC, WARN_HASH_WEAK]], - 'hmac-ripemd160': [['2.5.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], - 'hmac-ripemd160@openssh.com': [['2.1.0', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_ENCRYPT_AND_MAC]], - 'umac-64@openssh.com': [['4.7'], [], [WARN_ENCRYPT_AND_MAC, WARN_TAG_SIZE]], - 'umac-128@openssh.com': [['6.2'], [], [WARN_ENCRYPT_AND_MAC]], - 'hmac-sha1-etm@openssh.com': [['6.2'], [], [WARN_HASH_WEAK]], - 'hmac-sha1-96-etm@openssh.com': [['6.2', '6.6', None], [FAIL_OPENSSH67_UNSAFE], [WARN_HASH_WEAK]], - 'hmac-sha2-256-etm@openssh.com': [['6.2']], - 'hmac-sha2-512-etm@openssh.com': [['6.2']], - 'hmac-md5-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], - 'hmac-md5-96-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY, WARN_HASH_WEAK]], - 'hmac-ripemd160-etm@openssh.com': [['6.2', '6.6', '7.1'], [FAIL_OPENSSH67_UNSAFE], [WARN_OPENSSH72_LEGACY]], - 'umac-64-etm@openssh.com': [['6.2'], [], [WARN_TAG_SIZE]], - 'umac-128-etm@openssh.com': [['6.2']], - } - } # type: Dict[str, Dict[str, List[List[str]]]] +class KexGroup14_SHA1(KexGroup14): + def __init__(self): + super(KexGroup14_SHA1, self).__init__('sha1') -def get_ssh_version(version_desc): - # type: (str) -> Tuple[str, str] - if version_desc.startswith('d'): - return (SSH.Product.DropbearSSH, version_desc[1:]) - elif version_desc.startswith('l1'): - return (SSH.Product.LibSSH, version_desc[2:]) - else: - return (SSH.Product.OpenSSH, version_desc) +class KexGroup14_SHA256(KexGroup14): + def __init__(self): + super(KexGroup14_SHA256, self).__init__('sha256') -def get_alg_timeframe(versions, for_server=True, result=None): - # type: (List[str], bool, Optional[Dict[str, List[Optional[str]]]]) -> Dict[str, List[Optional[str]]] - result = result or {} - vlen = len(versions) - for i in range(3): - if i > vlen - 1: - if i == 2 and vlen > 1: - cversions = versions[1] - else: - continue - else: - cversions = versions[i] - if cversions is None: - continue - for v in cversions.split(','): - ssh_prefix, ssh_version = get_ssh_version(v) - if not ssh_version: - continue - if ssh_version.endswith('C'): - if for_server: - continue - ssh_version = ssh_version[:-1] - if ssh_prefix not in result: - result[ssh_prefix] = [None, None, None] - prev, push = result[ssh_prefix][i], False - if prev is None: - push = True - elif i == 0 and prev < ssh_version: - push = True - elif i > 0 and prev > ssh_version: - push = True - if push: - result[ssh_prefix][i] = ssh_version - return result +class KexGroup16_SHA512(KexDH): + def __init__(self): + # rfc3526: 4096-bit modp group + p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67' + 'cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6d' + 'f25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff' + '5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3d' + 'c2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3' + 'ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08' + 'ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c5' + '5df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa0510' + '15728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458db' + 'ef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e0' + '4a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f' + '2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab31' + '43db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba' + '5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db' + '04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233b' + 'a186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa9' + '93b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199ffffffffffff' + 'ffff', 16) + super(KexGroup16_SHA512, self).__init__('KexGroup16_SHA512', 'sha512', 2, p) -def get_ssh_timeframe(alg_pairs, for_server=True): - # type: (List[Tuple[int, Dict[str, Dict[str, List[List[str]]]], List[Tuple[str, List[text_type]]]]], bool) -> Dict[str, List[Optional[str]]] - timeframe = {} # type: Dict[str, List[Optional[str]]] - for alg_pair in alg_pairs: - alg_db = alg_pair[1] - for alg_set in alg_pair[2]: - alg_type, alg_list = alg_set - for alg_name in alg_list: - alg_name_native = utils.to_ntext(alg_name) - alg_desc = alg_db[alg_type].get(alg_name_native) - if alg_desc is None: - continue - versions = alg_desc[0] - timeframe = get_alg_timeframe(versions, for_server, timeframe) - return timeframe +class KexGroup18_SHA512(KexDH): + def __init__(self): + # rfc3526: 8192-bit modp group + p = int('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67' + 'cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6d' + 'f25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff' + '5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3d' + 'c2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3' + 'ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08' + 'ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c5' + '5df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa0510' + '15728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458db' + 'ef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e0' + '4a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f' + '2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab31' + '43db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba' + '5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db' + '04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233b' + 'a186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa9' + '93b4ea988d8fddc186ffb7dc90a6c08f4df435c93402849236c3fab4d27c' + '7026c1d4dcb2602646dec9751e763dba37bdf8ff9406ad9e530ee5db382f' + '413001aeb06a53ed9027d831179727b0865a8918da3edbebcf9b14ed44ce' + '6cbaced4bb1bdb7f1447e6cc254b332051512bd7af426fb8f401378cd2bf' + '5983ca01c64b92ecf032ea15d1721d03f482d7ce6e74fef6d55e702f4698' + '0c82b5a84031900b1c9e59e7c97fbec7e8f323a97a7e36cc88be0f1d45b7' + 'ff585ac54bd407b22b4154aacc8f6d7ebf48e1d814cc5ed20f8037e0a797' + '15eef29be32806a1d58bb7c5da76f550aa3d8a1fbff0eb19ccb1a313d55c' + 'da56c9ec2ef29632387fe8d76e3c0468043e8f663f4860ee12bf2d5b0b74' + '74d6e694f91e6dbe115974a3926f12fee5e438777cb6a932df8cd8bec4d0' + '73b931ba3bc832b68d9dd300741fa7bf8afc47ed2576f6936ba424663aab' + '639c5ae4f5683423b4742bf1c978238f16cbe39d652de3fdb8befc848ad9' + '22222e04a4037c0713eb57a81a23f0c73473fc646cea306b4bcbc8862f83' + '85ddfa9d4b7fa2c087e879683303ed5bdd3a062b3cf5b3a278a66d2a13f8' + '3f44f82ddf310ee074ab6a364597e899a0255dc164f31cc50846851df9ab' + '48195ded7ea1b1d510bd7ee74d73faf36bc31ecfa268359046f4eb879f92' + '4009438b481c6cd7889a002ed5ee382bc9190da6fc026e479558e4475677' + 'e9aa9e3050e2765694dfc81f56e880b96e7160c980dd98edd3dfffffffff' + 'ffffffff', 16) + super(KexGroup18_SHA512, self).__init__('KexGroup18_SHA512', 'sha512', 2, p) -def get_alg_since_text(versions): - # type: (List[str]) -> text_type - tv = [] - if len(versions) == 0 or versions[0] is None: - return None - for v in versions[0].split(','): - ssh_prefix, ssh_version = get_ssh_version(v) - if not ssh_version: - continue - if ssh_prefix in [SSH.Product.LibSSH]: - continue - if ssh_version.endswith('C'): - ssh_version = '{0} (client only)'.format(ssh_version[:-1]) - tv.append('{0} {1}'.format(ssh_prefix, ssh_version)) - if len(tv) == 0: - return None - return 'available since ' + ', '.join(tv).rstrip(', ') +class KexCurve25519_SHA256(KexDH): + def __init__(self): + super(KexCurve25519_SHA256, self).__init__('KexCurve25519_SHA256', 'sha256', 0, 0) + + # To start an ED25519 kex, we simply send a random 256-bit number as the + # public key. + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): + self.__ed25519_pubkey = os.urandom(32) + s.write_byte(init_msg) + s.write_string(self.__ed25519_pubkey) + s.send_packet() -def get_alg_pairs(kex, pkm): - # type: (Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> List[Tuple[int, Dict[str, Dict[str, List[List[str]]]], List[Tuple[str, List[text_type]]]]] - alg_pairs = [] - if pkm is not None: - alg_pairs.append((1, SSH1.KexDB.ALGORITHMS, - [('key', [u'ssh-rsa1']), - ('enc', pkm.supported_ciphers), - ('aut', pkm.supported_authentications)])) - if kex is not None: - alg_pairs.append((2, KexDB.ALGORITHMS, - [('kex', kex.kex_algorithms), - ('key', kex.key_algorithms), - ('enc', kex.server.encryption), - ('mac', kex.server.mac)])) - return alg_pairs +class KexNISTP256(KexDH): + def __init__(self): + super(KexNISTP256, self).__init__('KexNISTP256', 'sha256', 0, 0) + + # Because the server checks that the value sent here is valid (i.e.: it lies + # on the curve, among other things), we would have to write a lot of code + # or import an elliptic curve library in order to randomly generate a + # valid elliptic point each time. Hence, we will simply send a static + # value, which is enough for us to extract the server's host key. + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): + s.write_byte(init_msg) + s.write_string(b'\x04\x0b\x60\x44\x9f\x8a\x11\x9e\xc7\x81\x0c\xa9\x98\xfc\xb7\x90\xaa\x6b\x26\x8c\x12\x4a\xc0\x09\xbb\xdf\xc4\x2c\x4c\x2c\x99\xb6\xe1\x71\xa0\xd4\xb3\x62\x47\x74\xb3\x39\x0c\xf2\x88\x4a\x84\x6b\x3b\x15\x77\xa5\x77\xd2\xa9\xc9\x94\xf9\xd5\x66\x19\xcd\x02\x34\xd1') + s.send_packet() -def get_alg_recommendations(software, kex, pkm, for_server=True): - # type: (SSH.Software, SSH2.Kex, SSH1.PublicKeyMessage, bool) -> Tuple[SSH.Software, Dict[int, Dict[str, Dict[str, Dict[str, int]]]]] - # pylint: disable=too-many-locals,too-many-statements - alg_pairs = get_alg_pairs(kex, pkm) - vproducts = [SSH.Product.OpenSSH, - SSH.Product.DropbearSSH, - SSH.Product.LibSSH] - if software is not None: - if software.product not in vproducts: - software = None - if software is None: - ssh_timeframe = get_ssh_timeframe(alg_pairs, for_server) - for product in vproducts: - if product not in ssh_timeframe: - continue - version = ssh_timeframe[product][0] - if version is not None: - software = SSH.Software(None, product, version, None, None) - break - rec = {} # type: Dict[int, Dict[str, Dict[str, Dict[str, int]]]] - if software is None: - return software, rec - for alg_pair in alg_pairs: - sshv, alg_db = alg_pair[0], alg_pair[1] - rec[sshv] = {} - for alg_set in alg_pair[2]: - alg_type, alg_list = alg_set - if alg_type == 'aut': - continue - rec[sshv][alg_type] = {'add': {}, 'del': {}} - for n, alg_desc in alg_db[alg_type].items(): - if alg_type == 'key' and '-cert-' in n: - continue - versions = alg_desc[0] - if len(versions) == 0 or versions[0] is None: - continue - matches = False - for v in versions[0].split(','): - ssh_prefix, ssh_version = get_ssh_version(v) - if not ssh_version: - continue - if ssh_prefix != software.product: - continue - if ssh_version.endswith('C'): - if for_server: - continue - ssh_version = ssh_version[:-1] - if software.compare_version(ssh_version) < 0: - continue - matches = True - break - if not matches: - continue - adl, faults = len(alg_desc), 0 - for i in range(1, 3): - if not adl > i: - continue - fc = len(alg_desc[i]) - if fc > 0: - faults += pow(10, 2 - i) * fc - if n not in alg_list: - if faults > 0: - continue - rec[sshv][alg_type]['add'][n] = 0 - else: - if faults == 0: - continue - if n == 'diffie-hellman-group-exchange-sha256': - if software.compare_version('7.3') < 0: - continue - rec[sshv][alg_type]['del'][n] = faults - add_count = len(rec[sshv][alg_type]['add']) - del_count = len(rec[sshv][alg_type]['del']) - new_alg_count = len(alg_list) + add_count - del_count - if new_alg_count < 1 and del_count > 0: - mf = min(rec[sshv][alg_type]['del'].values()) - new_del = {} - for k, cf in rec[sshv][alg_type]['del'].items(): - if cf != mf: - new_del[k] = cf - if del_count != len(new_del): - rec[sshv][alg_type]['del'] = new_del - new_alg_count += del_count - len(new_del) - if new_alg_count < 1: - del rec[sshv][alg_type] - else: - if add_count == 0: - del rec[sshv][alg_type]['add'] - if del_count == 0: - del rec[sshv][alg_type]['del'] - if len(rec[sshv][alg_type]) == 0: - del rec[sshv][alg_type] - if len(rec[sshv]) == 0: - del rec[sshv] - return software, rec +class KexNISTP384(KexDH): + def __init__(self): + super(KexNISTP384, self).__init__('KexNISTP384', 'sha256', 0, 0) + + # See comment for KexNISTP256.send_init(). + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): + s.write_byte(init_msg) + s.write_string(b'\x04\xe2\x9b\x84\xce\xa1\x39\x50\xfe\x1e\xa3\x18\x70\x1c\xe2\x7a\xe4\xb5\x6f\xdf\x93\x9f\xd4\xf4\x08\xcc\x9b\x02\x10\xa4\xca\x77\x9c\x2e\x51\x44\x1d\x50\x7a\x65\x4e\x7e\x2f\x10\x2d\x2d\x4a\x32\xc9\x8e\x18\x75\x90\x6c\x19\x10\xda\xcc\xa8\xe9\xf4\xc4\x3a\x53\x80\x35\xf4\x97\x9c\x04\x16\xf9\x5a\xdc\xcc\x05\x94\x29\xfa\xc4\xd6\x87\x4e\x13\x21\xdb\x3d\x12\xac\xbd\x20\x3b\x60\xff\xe6\x58\x42') + s.send_packet() -def output_algorithms(title, alg_db, alg_type, algorithms, maxlen=0): - # type: (str, Dict[str, Dict[str, List[List[str]]]], str, List[text_type], int) -> None +class KexNISTP521(KexDH): + def __init__(self): + super(KexNISTP521, self).__init__('KexNISTP521', 'sha256', 0, 0) + + # See comment for KexNISTP256.send_init(). + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_INIT): + s.write_byte(init_msg) + s.write_string(b'\x04\x01\x02\x90\x29\xe9\x8f\xa8\x04\xaf\x1c\x00\xf9\xc6\x29\xc0\x39\x74\x8e\xea\x47\x7e\x7c\xf7\x15\x6e\x43\x3b\x59\x13\x53\x43\xb0\xae\x0b\xe7\xe6\x7c\x55\x73\x52\xa5\x2a\xc1\x42\xde\xfc\xf4\x1f\x8b\x5a\x8d\xfa\xcd\x0a\x65\x77\xa8\xce\x68\xd2\xc6\x26\xb5\x3f\xee\x4b\x01\x7b\xd2\x96\x23\x69\x53\xc7\x01\xe1\x0d\x39\xe9\x87\x49\x3b\xc8\xec\xda\x0c\xf9\xca\xad\x89\x42\x36\x6f\x93\x78\x78\x31\x55\x51\x09\x51\xc0\x96\xd7\xea\x61\xbf\xc2\x44\x08\x80\x43\xed\xc6\xbb\xfb\x94\xbd\xf8\xdf\x2b\xd8\x0b\x2e\x29\x1b\x8c\xc4\x8a\x04\x2d\x3a') + s.send_packet() + + +class KexGroupExchange(KexDH): + def __init__(self, classname, hash_alg): + super(KexGroupExchange, self).__init__(classname, hash_alg, 0, 0) + + def send_init(self, s, init_msg=SSH.Protocol.MSG_KEXDH_GEX_REQUEST): + self.send_init_gex(s) + + # The group exchange starts with sending a message to the server with + # the minimum, maximum, and preferred number of bits are for the DH group. + # The server responds with a generator and prime modulus that matches that, + # then the handshake continues on like a normal DH handshake (except the + # SSH message types differ). + def send_init_gex(self, s, minbits=1024, prefbits=2048, maxbits=8192): + + # Send the initial group exchange request. Tell the server what range + # of modulus sizes we will accept, along with our preference. + s.write_byte(SSH.Protocol.MSG_KEXDH_GEX_REQUEST) + s.write_int(minbits) + s.write_int(prefbits) + s.write_int(maxbits) + s.send_packet() + + packet_type, payload = s.read_packet(2) + if (packet_type != SSH.Protocol.MSG_KEXDH_GEX_GROUP) and (packet_type != SSH.Protocol.MSG_DEBUG): + # TODO: replace with a better exception type. + raise Exception('Expected MSG_KEXDH_GEX_REPLY (%d), but got %d instead.' % (SSH.Protocol.MSG_KEXDH_GEX_REPLY, packet_type)) + + # Skip any & all MSG_DEBUG messages. + while packet_type == SSH.Protocol.MSG_DEBUG: + packet_type, payload = s.read_packet(2) + + # Parse the modulus (p) and generator (g) values from the server. + ptr = 0 + p_len = struct.unpack('>I', payload[ptr:ptr + 4])[0] + ptr += 4 + + p = int(binascii.hexlify(payload[ptr:ptr + p_len]), 16) + ptr += p_len + + g_len = struct.unpack('>I', payload[ptr:ptr + 4])[0] + ptr += 4 + + g = int(binascii.hexlify(payload[ptr:ptr + g_len]), 16) + ptr += g_len + + # Now that we got the generator and modulus, perform the DH exchange + # like usual. + super(KexGroupExchange, self).set_params(g, p) + super(KexGroupExchange, self).send_init(s, SSH.Protocol.MSG_KEXDH_GEX_INIT) + + +class KexGroupExchange_SHA1(KexGroupExchange): + def __init__(self): + super(KexGroupExchange_SHA1, self).__init__('KexGroupExchange_SHA1', 'sha1') + + +class KexGroupExchange_SHA256(KexGroupExchange): + def __init__(self): + super(KexGroupExchange_SHA256, self).__init__('KexGroupExchange_SHA256', 'sha256') + + +def output_algorithms(title, alg_db, alg_type, algorithms, unknown_algs, maxlen=0, alg_sizes=None): + # type: (str, Dict[str, Dict[str, List[List[Optional[str]]]]], str, List[text_type], int) -> None with OutputBuffer() as obuf: for algorithm in algorithms: - output_algorithm(alg_db, alg_type, algorithm, maxlen) + output_algorithm(alg_db, alg_type, algorithm, unknown_algs, maxlen, alg_sizes) if len(obuf) > 0: out.head('# ' + title) obuf.flush() out.sep() -def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): - # type: (Dict[str, Dict[str, List[List[str]]]], str, text_type, int) -> None +def output_algorithm(alg_db, alg_type, alg_name, unknown_algs, alg_max_len=0, alg_sizes=None): + # type: (Dict[str, Dict[str, List[List[Optional[str]]]]], str, text_type, int) -> None prefix = '(' + alg_type + ') ' if alg_max_len == 0: alg_max_len = len(alg_name) padding = '' if out.batch else ' ' * (alg_max_len - len(alg_name)) + + # If this is an RSA host key or DH GEX, append the size to its name and fix + # the padding. + alg_name_with_size = None + if (alg_sizes is not None) and (alg_name in alg_sizes): + hostkey_size, ca_size = alg_sizes[alg_name] + if ca_size > 0: + alg_name_with_size = '%s (%d-bit cert/%d-bit CA)' % (alg_name, hostkey_size, ca_size) + padding = padding[0:-15] + else: + alg_name_with_size = '%s (%d-bit)' % (alg_name, hostkey_size) + padding = padding[0:-11] + texts = [] if len(alg_name.strip()) == 0: return @@ -1677,59 +2698,72 @@ def output_algorithm(alg_db, alg_type, alg_name, alg_max_len=0): for idx, level in enumerate(['fail', 'warn', 'info']): if level == 'info': versions = alg_desc[0] - since_text = get_alg_since_text(versions) - if since_text: + since_text = SSH.Algorithm.get_since_text(versions) + if since_text is not None and len(since_text) > 0: texts.append((level, since_text)) idx = idx + 1 if ldesc > idx: for t in alg_desc[idx]: + if t is None: + continue texts.append((level, t)) if len(texts) == 0: texts.append(('info', '')) else: texts.append(('warn', 'unknown algorithm')) + unknown_algs.append(alg_name) + + alg_name = alg_name_with_size if alg_name_with_size is not None else alg_name first = True - for (level, text) in texts: + for level, text in texts: f = getattr(out, level) - text = '[' + level + '] ' + text + comment = (padding + ' -- [' + level + '] ' + text) if text != '' else '' if first: if first and level == 'info': f = out.good - f(prefix + alg_name + padding + ' -- ' + text) + f(prefix + alg_name + comment) first = False - else: + else: # pylint: disable=else-if-used if out.verbose: - f(prefix + alg_name + padding + ' -- ' + text) - else: - f(' ' * len(prefix + alg_name) + padding + ' `- ' + text) + f(prefix + alg_name + comment) + elif text != '': + comment = (padding + ' `- [' + level + '] ' + text) + f(' ' * len(prefix + alg_name) + comment) -def output_compatibility(kex, pkm, for_server=True): - # type: (Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage], bool) -> None - alg_pairs = get_alg_pairs(kex, pkm) - ssh_timeframe = get_ssh_timeframe(alg_pairs, for_server) - vp = 1 if for_server else 2 +def output_compatibility(algs, client_audit, for_server=True): + # type: (SSH.Algorithms, bool) -> None + + # Don't output any compatibility info if we're doing a client audit. + if client_audit: + return + + ssh_timeframe = algs.get_ssh_timeframe(for_server) comp_text = [] - for sshd_name in [SSH.Product.OpenSSH, SSH.Product.DropbearSSH]: - if sshd_name not in ssh_timeframe: + for ssh_prod in [SSH.Product.OpenSSH, SSH.Product.DropbearSSH]: + if ssh_prod not in ssh_timeframe: continue - v = ssh_timeframe[sshd_name] - if v[vp] is None: - comp_text.append('{0} {1}+'.format(sshd_name, v[0])) - elif v[0] == v[vp]: - comp_text.append('{0} {1}'.format(sshd_name, v[0])) + v_from = ssh_timeframe.get_from(ssh_prod, for_server) + v_till = ssh_timeframe.get_till(ssh_prod, for_server) + if v_from is None: + continue + if v_till is None: + comp_text.append('{0} {1}+'.format(ssh_prod, v_from)) + elif v_from == v_till: + comp_text.append('{0} {1}'.format(ssh_prod, v_from)) else: - if v[vp] < v[0]: + software = SSH.Software(None, ssh_prod, v_from, None, None) + if software.compare_version(v_till) > 0: tfmt = '{0} {1}+ (some functionality from {2})' else: tfmt = '{0} {1}-{2}' - comp_text.append(tfmt.format(sshd_name, v[0], v[vp])) + comp_text.append(tfmt.format(ssh_prod, v_from, v_till)) if len(comp_text) > 0: out.good('(gen) compatibility: ' + ', '.join(comp_text)) -def output_security_sub(sub, software, padlen): - # type: (str, SSH.Software, int) -> None +def output_security_sub(sub, software, client_audit, padlen): + # type: (str, Optional[SSH.Software], int) -> None secdb = SSH.Security.CVE if sub == 'cve' else SSH.Security.TXT if software is None or software.product not in secdb: return @@ -1738,87 +2772,172 @@ def output_security_sub(sub, software, padlen): if not software.between_versions(vfrom, vtill): continue target, name = line[2:4] # type: int, str - is_server, is_client = target & 1 == 1, target & 2 == 2 - is_local = target & 4 == 4 - if not is_server: + is_server = target & 1 == 1 + is_client = target & 2 == 2 + # is_local = target & 4 == 4 + + # If this security entry applies only to servers, but we're testing a client, then skip it. Similarly, skip entries that apply only to clients, but we're testing a server. + if (is_server and not is_client and client_audit) or (is_client and not is_server and not client_audit): continue p = '' if out.batch else ' ' * (padlen - len(name)) if sub == 'cve': cvss, descr = line[4:6] # type: float, str - out.fail('(cve) {0}{1} -- ({2}) {3}'.format(name, p, cvss, descr)) + + # Critical CVSS scores (>= 8.0) are printed as a fail, otherwise they are printed as a warning. + out_func = out.warn + if cvss >= 8.0: + out_func = out.fail + out_func('(cve) {0}{1} -- (CVSSv2: {2}) {3}'.format(name, p, cvss, descr)) else: descr = line[4] out.fail('(sec) {0}{1} -- {2}'.format(name, p, descr)) -def output_security(banner, padlen): - # type: (SSH.Banner, int) -> None +def output_security(banner, client_audit, padlen): + # type: (Optional[SSH.Banner], int) -> None with OutputBuffer() as obuf: - if banner: + if banner is not None: software = SSH.Software.parse(banner) - output_security_sub('cve', software, padlen) - output_security_sub('txt', software, padlen) + output_security_sub('cve', software, client_audit, padlen) + output_security_sub('txt', software, client_audit, padlen) if len(obuf) > 0: out.head('# security') obuf.flush() out.sep() -def output_fingerprint(kex, pkm, sha256=True, padlen=0): - # type: (Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage], bool, int) -> None +def output_fingerprints(algs, sha256=True): + # type: (SSH.Algorithms, bool, int) -> None with OutputBuffer() as obuf: fps = [] - if pkm is not None: + if algs.ssh1kex is not None: name = 'ssh-rsa1' - fp = SSH.Fingerprint(pkm.host_key_fingerprint_data) - bits = pkm.host_key_bits - fps.append((name, fp, bits)) + fp = SSH.Fingerprint(algs.ssh1kex.host_key_fingerprint_data) + #bits = algs.ssh1kex.host_key_bits + fps.append((name, fp)) + if algs.ssh2kex is not None: + host_keys = algs.ssh2kex.host_keys() + for host_key_type in algs.ssh2kex.host_keys(): + if host_keys[host_key_type] is None: + continue + + fp = SSH.Fingerprint(host_keys[host_key_type]) + + # Workaround for Python's order-indifference in dicts. We might get a random RSA type (ssh-rsa, rsa-sha2-256, or rsa-sha2-512), so running the tool against the same server three times may give three different host key types here. So if we have any RSA type, we will simply hard-code it to 'ssh-rsa'. + if host_key_type in SSH2.HostKeyTest.RSA_FAMILY: + host_key_type = 'ssh-rsa' + + # Skip over certificate host types (or we would return invalid fingerprints). + if '-cert-' not in host_key_type: + fps.append((host_key_type, fp)) + # Similarly, the host keys can be processed in random order due to Python's order-indifference in dicts. So we sort this list before printing; this makes automated testing possible. + fps = sorted(fps) for fpp in fps: - name, fp, bits = fpp + name, fp = fpp fpo = fp.sha256 if sha256 else fp.md5 - p = '' if out.batch else ' ' * (padlen - len(name)) - out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo)) + #p = '' if out.batch else ' ' * (padlen - len(name)) + #out.good('(fin) {0}{1} -- {2} {3}'.format(name, p, bits, fpo)) + out.good('(fin) {0}: {1}'.format(name, fpo)) if len(obuf) > 0: out.head('# fingerprints') obuf.flush() out.sep() -def output_recommendations(software, kex, pkm, padlen=0): - # type: (SSH.Software, SSH2.Kex, SSH1.PublicKeyMessage, int) -> None +# Returns True if no warnings or failures encountered in configuration. +def output_recommendations(algs, software, padlen=0): + # type: (SSH.Algorithms, Optional[SSH.Software], int) -> None + + ret = True + # PuTTY's algorithms cannot be modified, so there's no point in issuing recommendations. + if (software is not None) and (software.product == SSH.Product.PuTTY): + max_vuln_version = 0.0 + max_cvssv2_severity = 0.0 + # Search the CVE database for the most recent vulnerable version and the max CVSSv2 score. + for cve_list in SSH.Security.CVE['PuTTY']: + vuln_version = float(cve_list[1]) + cvssv2_severity = cve_list[4] + + if vuln_version > max_vuln_version: + max_vuln_version = vuln_version + if cvssv2_severity > max_cvssv2_severity: + max_cvssv2_severity = cvssv2_severity + + fn = out.warn + if max_cvssv2_severity > 8.0: + fn = out.fail + + # Assuming that PuTTY versions will always increment by 0.01, we can calculate the first safe version by adding 0.01 to the latest vulnerable version. + current_version = float(software.version) + upgrade_to_version = max_vuln_version + 0.01 + if current_version < upgrade_to_version: + out.head('# recommendations') + fn('(rec) Upgrade to PuTTY v%.2f' % upgrade_to_version) + out.sep() + ret = False + return ret + for_server = True with OutputBuffer() as obuf: - software, alg_rec = get_alg_recommendations(software, kex, pkm, for_server) + software, alg_rec = algs.get_recommendations(software, for_server) for sshv in range(2, 0, -1): if sshv not in alg_rec: continue for alg_type in ['kex', 'key', 'enc', 'mac']: if alg_type not in alg_rec[sshv]: continue - for action in ['del', 'add']: + for action in ['del', 'add', 'chg']: if action not in alg_rec[sshv][alg_type]: continue for name in alg_rec[sshv][alg_type][action]: p = '' if out.batch else ' ' * (padlen - len(name)) + chg_additional_info = '' if action == 'del': an, sg, fn = 'remove', '-', out.warn + ret = False if alg_rec[sshv][alg_type][action][name] >= 10: fn = out.fail - else: + elif action == 'add': an, sg, fn = 'append', '+', out.good + elif action == 'chg': + an, sg, fn = 'change', '!', out.fail + ret = False + chg_additional_info = ' (increase modulus size to 2048 bits or larger)' b = '(SSH{0})'.format(sshv) if sshv == 1 else '' - fm = '(rec) {0}{1}{2}-- {3} algorithm to {4} {5}' - fn(fm.format(sg, name, p, alg_type, an, b)) + fm = '(rec) {0}{1}{2}-- {3} algorithm to {4}{5} {6}' + fn(fm.format(sg, name, p, alg_type, an, chg_additional_info, b)) if len(obuf) > 0: - title = '(for {0})'.format(software.display(False)) if software else '' + if software is not None: + title = '(for {0})'.format(software.display(False)) + else: + title = '' out.head('# algorithm recommendations {0}'.format(title)) + obuf.flush(True) # Sort the output so that it is always stable (needed for repeatable testing). + out.sep() + return ret + + +# Output additional information & notes. +def output_info(algs, software, client_audit, any_problems, padlen=0): + with OutputBuffer() as obuf: + # Tell user that PuTTY cannot be hardened at the protocol-level. + if client_audit and (software is not None) and (software.product == SSH.Product.PuTTY): + out.warn('(nfo) PuTTY does not have the option of restricting any algorithms during the SSH handshake.') + + # If any warnings or failures were given, print a link to the hardening guides. + if any_problems: + out.warn('(nfo) For hardening guides on common OSes, please see: ') + + if len(obuf) > 0: + out.head('# additional info') obuf.flush() out.sep() -def output(banner, header, kex=None, pkm=None): +def output(banner, header, client_audit=False, kex=None, pkm=None): # type: (Optional[SSH.Banner], List[text_type], Optional[SSH2.Kex], Optional[SSH1.PublicKeyMessage]) -> None - sshv = 1 if pkm else 2 + sshv = 1 if pkm is not None else 2 + algs = SSH.Algorithms(pkm, kex) with OutputBuffer() as obuf: if len(header) > 0: out.info('(gen) header: ' + '\n'.join(header)) @@ -1834,7 +2953,7 @@ def output(banner, header, kex=None, pkm=None): out.good('(gen) software: {0}'.format(software)) else: software = None - output_compatibility(kex, pkm) + output_compatibility(algs, client_audit) if kex is not None: compressions = [x for x in kex.server.compression if x != 'none'] if len(compressions) > 0: @@ -1846,43 +2965,38 @@ def output(banner, header, kex=None, pkm=None): out.head('# general') obuf.flush() out.sep() - ml, maxlen = lambda l: max(len(i) for i in l), 0 - if pkm is not None: - maxlen = max(ml(pkm.supported_ciphers), - ml(pkm.supported_authentications), - maxlen) - if kex is not None: - maxlen = max(ml(kex.kex_algorithms), - ml(kex.key_algorithms), - ml(kex.server.encryption), - ml(kex.server.mac), - maxlen) - maxlen += 1 - output_security(banner, maxlen) + maxlen = algs.maxlen + 1 + output_security(banner, client_audit, maxlen) + unknown_algorithms = [] # Filled in by output_algorithms() with unidentified algs. if pkm is not None: adb = SSH1.KexDB.ALGORITHMS ciphers = pkm.supported_ciphers auths = pkm.supported_authentications title, atype = 'SSH1 host-key algorithms', 'key' - output_algorithms(title, adb, atype, ['ssh-rsa1'], maxlen) + output_algorithms(title, adb, atype, ['ssh-rsa1'], unknown_algorithms, maxlen) title, atype = 'SSH1 encryption algorithms (ciphers)', 'enc' - output_algorithms(title, adb, atype, ciphers, maxlen) + output_algorithms(title, adb, atype, ciphers, unknown_algorithms, maxlen) title, atype = 'SSH1 authentication types', 'aut' - output_algorithms(title, adb, atype, auths, maxlen) + output_algorithms(title, adb, atype, auths, unknown_algorithms, maxlen) if kex is not None: - adb = KexDB.ALGORITHMS + adb = SSH2.KexDB.ALGORITHMS title, atype = 'key exchange algorithms', 'kex' - output_algorithms(title, adb, atype, kex.kex_algorithms, maxlen) + output_algorithms(title, adb, atype, kex.kex_algorithms, unknown_algorithms, maxlen, kex.dh_modulus_sizes()) title, atype = 'host-key algorithms', 'key' - output_algorithms(title, adb, atype, kex.key_algorithms, maxlen) + output_algorithms(title, adb, atype, kex.key_algorithms, unknown_algorithms, maxlen, kex.rsa_key_sizes()) title, atype = 'encryption algorithms (ciphers)', 'enc' - output_algorithms(title, adb, atype, kex.server.encryption, maxlen) + output_algorithms(title, adb, atype, kex.server.encryption, unknown_algorithms, maxlen) title, atype = 'message authentication code algorithms', 'mac' - output_algorithms(title, adb, atype, kex.server.mac, maxlen) - output_recommendations(software, kex, pkm, maxlen) - output_fingerprint(kex, pkm, True, maxlen) + output_algorithms(title, adb, atype, kex.server.mac, unknown_algorithms, maxlen) + output_fingerprints(algs, True) + perfect_config = output_recommendations(algs, software, maxlen) + output_info(algs, software, client_audit, not perfect_config) + # If we encountered any unknown algorithms, ask the user to report them. + if len(unknown_algorithms) > 0: + out.warn("\n\n!!! WARNING: unknown algorithm(s) found!: %s. Please email the full output above to the maintainer (jtesta@positronsecurity.com), or create a Github issue at .\n" % ','.join(unknown_algorithms)) + class Utils(object): @classmethod def _type_err(cls, v, target): @@ -1913,28 +3027,58 @@ class Utils(object): if isinstance(v, str): return v elif isinstance(v, text_type): - return v.encode(enc) + return v.encode(enc) # PY2 only elif isinstance(v, binary_type): - return v.decode(enc) + return v.decode(enc) # PY3 only raise cls._type_err(v, 'native text') + @classmethod + def _is_ascii(cls, v, char_filter=lambda x: x <= 127): + # type: (Union[text_type, str], Callable[[int], bool]) -> bool + r = False + if isinstance(v, (text_type, str)): + for c in v: + i = cls.ctoi(c) + if not char_filter(i): + return r + r = True + return r + + @classmethod + def _to_ascii(cls, v, char_filter=lambda x: x <= 127, errors='replace'): + # type: (Union[text_type, str], Callable[[int], bool], str) -> str + if isinstance(v, (text_type, str)): + r = bytearray() + for c in v: + i = cls.ctoi(c) + if char_filter(i): + r.append(i) + else: + if errors == 'ignore': + continue + r.append(63) + return cls.to_ntext(r.decode('ascii')) + raise cls._type_err(v, 'ascii') + @classmethod def is_ascii(cls, v): # type: (Union[text_type, str]) -> bool - try: - if isinstance(v, (text_type, str)): - v.encode('ascii') - return True - except UnicodeEncodeError: - pass - return False + return cls._is_ascii(v) @classmethod def to_ascii(cls, v, errors='replace'): # type: (Union[text_type, str], str) -> str - if isinstance(v, (text_type, str)): - return cls.to_ntext(v.encode('ascii', errors)) - raise cls._type_err(v, 'ascii') + return cls._to_ascii(v, errors=errors) + + @classmethod + def is_print_ascii(cls, v): + # type: (Union[text_type, str]) -> bool + return cls._is_ascii(v, lambda x: x >= 32 and x <= 126) + + @classmethod + def to_print_ascii(cls, v, errors='replace'): + # type: (Union[text_type, str], str) -> str + return cls._to_ascii(v, lambda x: x >= 32 and x <= 126, errors) @classmethod def unique_seq(cls, seq): @@ -1950,7 +3094,15 @@ class Utils(object): return tuple(x for x in seq if x not in seen and not _seen_add(x)) else: return [x for x in seq if x not in seen and not _seen_add(x)] - + + @classmethod + def ctoi(cls, c): + # type: (Union[text_type, str, int]) -> int + if isinstance(c, (text_type, str)): + return ord(c[0]) + else: + return c + @staticmethod def parse_int(v): # type: (Any) -> int @@ -1959,26 +3111,43 @@ class Utils(object): except: # pylint: disable=bare-except return 0 + @staticmethod + def parse_float(v): + # type: (Any) -> float + try: + return float(v) + except: # pylint: disable=bare-except + return -1.0 + def audit(aconf, sshv=None): # type: (AuditConf, Optional[int]) -> None out.batch = aconf.batch - out.colors = aconf.colors out.verbose = aconf.verbose - out.minlevel = aconf.minlevel - s = SSH.Socket(aconf.host, aconf.port) - s.connect(aconf.ipvo) + out.level = aconf.level + out.use_colors = aconf.colors + s = SSH.Socket(aconf.host, aconf.port, aconf.ipvo, aconf.timeout) + if aconf.client_audit: + s.listen_and_accept() + else: + s.connect() if sshv is None: sshv = 2 if aconf.ssh2 else 1 err = None - banner, header = s.get_banner(sshv) + banner, header, err = s.get_banner(sshv) if banner is None: - err = '[exception] did not receive banner.' + if err is None: + err = '[exception] did not receive banner.' + else: + err = '[exception] did not receive banner: {0}'.format(err) if err is None: packet_type, payload = s.read_packet(sshv) if packet_type < 0: try: - payload_txt = payload.decode('utf-8') if payload else u'empty' + if payload is not None and len(payload) > 0: + payload_txt = payload.decode('utf-8') + else: + payload_txt = u'empty' except UnicodeDecodeError: payload_txt = u'"{0}"'.format(repr(payload).lstrip('b')[1:-1]) if payload_txt == u'Protocol major versions differ.': @@ -1996,7 +3165,7 @@ def audit(aconf, sshv=None): fmt = '[exception] did not receive {0} ({1}), ' + \ 'instead received unknown message ({2})' err = fmt.format(err_pair[0], err_pair[1], packet_type) - if err: + if err is not None: output(banner, header) out.fail(err) sys.exit(1) @@ -2005,11 +3174,18 @@ def audit(aconf, sshv=None): output(banner, header, pkm=pkm) elif sshv == 2: kex = SSH2.Kex.parse(payload) - output(banner, header, kex=kex) + if aconf.client_audit is False: + SSH2.HostKeyTest.run(s, kex) + SSH2.GEXTest.run(s, kex) + output(banner, header, client_audit=aconf.client_audit, kex=kex) utils = Utils() out = Output() -if __name__ == '__main__': # pragma: nocover + +def main(): conf = AuditConf.from_cmdline(sys.argv[1:], usage) audit(conf) + +if __name__ == '__main__': # pragma: nocover + main() diff --git a/test/conftest.py b/test/conftest.py index 524c0fa..0bc4124 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -40,6 +40,41 @@ def output_spy(): return _OutputSpy() +class _VirtualGlobalSocket(object): + def __init__(self, vsocket): + self.vsocket = vsocket + self.addrinfodata = {} + + # pylint: disable=unused-argument + def create_connection(self, address, timeout=0, source_address=None): + # pylint: disable=protected-access + return self.vsocket._connect(address, True) + + # pylint: disable=unused-argument + def socket(self, + family=socket.AF_INET, + socktype=socket.SOCK_STREAM, + proto=0, + fileno=None): + return self.vsocket + + def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): + key = '{0}#{1}'.format(host, port) + if key in self.addrinfodata: + data = self.addrinfodata[key] + if isinstance(data, Exception): + raise data + return data + if host == 'localhost': + r = [] + if family in (0, socket.AF_INET): + r.append((socket.AF_INET, 1, 6, '', ('127.0.0.1', port))) + if family in (0, socket.AF_INET6): + r.append((socket.AF_INET6, 1, 6, '', ('::1', port))) + return r + return [] + + class _VirtualSocket(object): def __init__(self): self.sock_address = ('127.0.0.1', 0) @@ -49,6 +84,7 @@ class _VirtualSocket(object): self.rdata = [] self.sdata = [] self.errors = {} + self.gsock = _VirtualGlobalSocket(self) def _check_err(self, method): method_error = self.errors.get(method) @@ -113,18 +149,8 @@ class _VirtualSocket(object): @pytest.fixture() def virtual_socket(monkeypatch): vsocket = _VirtualSocket() - - # pylint: disable=unused-argument - def _socket(family=socket.AF_INET, - socktype=socket.SOCK_STREAM, - proto=0, - fileno=None): - return vsocket - - def _cc(address, timeout=0, source_address=None): - # pylint: disable=protected-access - return vsocket._connect(address, True) - - monkeypatch.setattr(socket, 'create_connection', _cc) - monkeypatch.setattr(socket, 'socket', _socket) + gsock = vsocket.gsock + monkeypatch.setattr(socket, 'create_connection', gsock.create_connection) + monkeypatch.setattr(socket, 'socket', gsock.socket) + monkeypatch.setattr(socket, 'getaddrinfo', gsock.getaddrinfo) return vsocket diff --git a/test/coverage.sh b/test/coverage.sh deleted file mode 100755 index 28f2010..0000000 --- a/test/coverage.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type py.test > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: py.test (Python testing framework) not found." - exit 1 -fi -cd -- "${_cdir}/.." -mkdir -p html -py.test -v --cov-report=html:html/coverage --cov=ssh-audit test diff --git a/test/docker/.ed25519.sk b/test/docker/.ed25519.sk new file mode 100644 index 0000000..58b097b --- /dev/null +++ b/test/docker/.ed25519.sk @@ -0,0 +1 @@ +iܛV违Z/D<|Sz=:1vu}Jݷ"^Bb&UP CJ? \ No newline at end of file diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile new file mode 100644 index 0000000..eef0139 --- /dev/null +++ b/test/docker/Dockerfile @@ -0,0 +1,32 @@ +FROM ubuntu:16.04 + +COPY openssh-4.0p1/sshd /openssh/sshd-4.0p1 +COPY openssh-5.6p1/sshd /openssh/sshd-5.6p1 +COPY openssh-8.0p1/sshd /openssh/sshd-8.0p1 +COPY dropbear-2019.78/dropbear /dropbear/dropbear-2019.78 +COPY tinyssh-20190101/build/bin/tinysshd /tinysshd/tinyssh-20190101 + +# Dropbear host keys. +COPY dropbear_*_host_key* /etc/dropbear/ + +# OpenSSH configs. +COPY sshd_config* /etc/ssh/ + +# OpenSSH host keys & moduli file. +COPY ssh_host_* /etc/ssh/ +COPY ssh1_host_* /etc/ssh/ +COPY moduli_1024 /usr/local/etc/moduli + +# TinySSH host keys. +COPY ed25519.pk /etc/tinyssh/ +COPY .ed25519.sk /etc/tinyssh/ + +COPY debug.sh /debug.sh + +RUN apt update 2> /dev/null +RUN apt install -y libssl-dev strace rsyslog ucspi-tcp 2> /dev/null +RUN apt clean 2> /dev/null +RUN useradd -s /bin/false sshd +RUN mkdir /var/empty + +EXPOSE 22 diff --git a/test/docker/debug.sh b/test/docker/debug.sh new file mode 100755 index 0000000..c4be343 --- /dev/null +++ b/test/docker/debug.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# This script is run on in docker container. It will enable logging for sshd in +# /var/log/auth.log. + +/etc/init.d/rsyslog start +sleep 1 +/openssh/sshd-5.6p1 -o LogLevel=DEBUG3 -f /etc/ssh/sshd_config-5.6p1_test1 +/bin/bash diff --git a/test/docker/dropbear_dss_host_key b/test/docker/dropbear_dss_host_key new file mode 100644 index 0000000..3388632 Binary files /dev/null and b/test/docker/dropbear_dss_host_key differ diff --git a/test/docker/dropbear_ecdsa_host_key b/test/docker/dropbear_ecdsa_host_key new file mode 100644 index 0000000..318ebb0 Binary files /dev/null and b/test/docker/dropbear_ecdsa_host_key differ diff --git a/test/docker/dropbear_rsa_host_key_1024 b/test/docker/dropbear_rsa_host_key_1024 new file mode 100644 index 0000000..d9ce331 Binary files /dev/null and b/test/docker/dropbear_rsa_host_key_1024 differ diff --git a/test/docker/dropbear_rsa_host_key_3072 b/test/docker/dropbear_rsa_host_key_3072 new file mode 100644 index 0000000..006249a Binary files /dev/null and b/test/docker/dropbear_rsa_host_key_3072 differ diff --git a/test/docker/ed25519.pk b/test/docker/ed25519.pk new file mode 100644 index 0000000..82cfb47 --- /dev/null +++ b/test/docker/ed25519.pk @@ -0,0 +1 @@ +1vu}Jݷ"^Bb&UP CJ? \ No newline at end of file diff --git a/test/docker/expected_results/dropbear_2019.78_test1.txt b/test/docker/expected_results/dropbear_2019.78_test1.txt new file mode 100644 index 0000000..f4ee85e --- /dev/null +++ b/test/docker/expected_results/dropbear_2019.78_test1.txt @@ -0,0 +1,85 @@ +# general +(gen) banner: SSH-2.0-dropbear_2019.78 +(gen) software: Dropbear SSH 2019.78 +(gen) compatibility: OpenSSH 7.4+ (some functionality from 6.6), Dropbear SSH 2018.76+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp521 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp384 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) diffie-hellman-group14-sha256 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) kexguess2@matt.ucc.asn.au -- [info] available since Dropbear SSH 2013.57 + +# host-key algorithms +(key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-dss -- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm + `- [warn] using small 1024-bit modulus + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) 3des-ctr -- [fail] using weak cipher + `- [info] available since Dropbear SSH 0.52 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 + +# message authentication code algorithms +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 + +# fingerprints +(fin) ssh-rsa: SHA256:CDfAU12pjQS7/91kg7gYacza0U/6PDbE04Ic3IpYxkM + +# algorithm recommendations (for Dropbear SSH 2019.78) +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -3des-ctr -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -ecdh-sha2-nistp256 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp384 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp521 -- kex algorithm to remove  +(rec) -ecdsa-sha2-nistp256 -- key algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -ssh-dss -- key algorithm to remove  +(rec) +diffie-hellman-group16-sha512 -- kex algorithm to append  +(rec) +twofish128-ctr -- enc algorithm to append  +(rec) +twofish256-ctr -- enc algorithm to append  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +(rec) -hmac-sha1 -- mac algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_4.0p1_test1.txt b/test/docker/expected_results/openssh_4.0p1_test1.txt new file mode 100644 index 0000000..1ab525a --- /dev/null +++ b/test/docker/expected_results/openssh_4.0p1_test1.txt @@ -0,0 +1,139 @@ +# general +(gen) banner: SSH-1.99-OpenSSH_4.0 +(gen) protocol SSH1 enabled +(gen) software: OpenSSH 4.0 +(gen) compatibility: OpenSSH 3.9-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values +(cve) CVE-2008-5161 -- (CVSSv2: 2.6) recover plaintext data from ciphertext +(cve) CVE-2008-4109 -- (CVSSv2: 5.0) cause DoS via multiple login attempts (slot exhaustion) +(cve) CVE-2008-1657 -- (CVSSv2: 6.5) bypass command restrictions via modifying session file +(cve) CVE-2008-1483 -- (CVSSv2: 6.9) hijack forwarded X11 connections +(cve) CVE-2007-4752 -- (CVSSv2: 7.5) privilege escalation via causing an X client to be trusted +(cve) CVE-2007-2243 -- (CVSSv2: 5.0) discover valid usernames through different responses +(cve) CVE-2006-5052 -- (CVSSv2: 5.0) discover valid usernames through different responses +(cve) CVE-2006-5051 -- (CVSSv2: 9.3) cause DoS or execute arbitrary code (double free) +(cve) CVE-2006-4924 -- (CVSSv2: 7.8) cause DoS via crafted packet (CPU consumption) +(cve) CVE-2006-0225 -- (CVSSv2: 4.6) execute arbitrary code +(cve) CVE-2005-2798 -- (CVSSv2: 5.0) leak data about authentication credentials + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-dss -- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm + `- [warn] using small 1024-bit modulus + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# encryption algorithms (ciphers) +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 4.0) +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -ssh-dss -- key algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test1.txt b/test/docker/expected_results/openssh_5.6p1_test1.txt new file mode 100644 index 0000000..72883da --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test1.txt @@ -0,0 +1,148 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 4.7-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-dss -- [fail] removed (in server) and disabled (in client) since OpenSSH 7.0, weak algorithm + `- [warn] using small 1024-bit modulus + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -ssh-dss -- key algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test2.txt b/test/docker/expected_results/openssh_5.6p1_test2.txt new file mode 100644 index 0000000..d952848 --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test2.txt @@ -0,0 +1,146 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/1024-bit CA) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa-cert-v01@openssh.com -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test3.txt b/test/docker/expected_results/openssh_5.6p1_test3.txt new file mode 100644 index 0000000..cb1217e --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test3.txt @@ -0,0 +1,146 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (1024-bit cert/3072-bit CA) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:YZ457EBcJTSxRKI3yXRgtAj3PBf5B9/F36b1SVooml4 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa-cert-v01@openssh.com -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test4.txt b/test/docker/expected_results/openssh_5.6p1_test4.txt new file mode 100644 index 0000000..9b84b6b --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test4.txt @@ -0,0 +1,144 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (3072-bit) -- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/1024-bit CA) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) !ssh-rsa-cert-v01@openssh.com -- key algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_5.6p1_test5.txt b/test/docker/expected_results/openssh_5.6p1_test5.txt new file mode 100644 index 0000000..e2e3479 --- /dev/null +++ b/test/docker/expected_results/openssh_5.6p1_test5.txt @@ -0,0 +1,142 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_5.6 +(gen) software: OpenSSH 5.6 +(gen) compatibility: OpenSSH 5.6-6.6, Dropbear SSH 0.53+ (some functionality from 0.52) +(gen) compression: enabled (zlib@openssh.com) + +# security +(cve) CVE-2016-3115 -- (CVSSv2: 5.5) bypass command restrictions via crafted X11 forwarding data +(cve) CVE-2016-1907 -- (CVSSv2: 5.0) cause DoS via crafted network traffic (out of bounds read) +(cve) CVE-2015-6564 -- (CVSSv2: 6.9) privilege escalation via leveraging sshd uid +(cve) CVE-2015-6563 -- (CVSSv2: 1.9) conduct impersonation attack +(cve) CVE-2014-2532 -- (CVSSv2: 5.8) bypass environment restrictions via specific string before wildcard +(cve) CVE-2014-1692 -- (CVSSv2: 7.5) cause DoS via triggering error condition (memory corruption) +(cve) CVE-2012-0814 -- (CVSSv2: 3.5) leak data via debug messages +(cve) CVE-2011-5000 -- (CVSSv2: 3.5) cause DoS via large value in certain length field (memory consumption) +(cve) CVE-2010-5107 -- (CVSSv2: 5.0) cause DoS via large number of connections (slot exhaustion) +(cve) CVE-2010-4755 -- (CVSSv2: 4.0) cause DoS via crafted glob expression (CPU and memory consumption) +(cve) CVE-2010-4478 -- (CVSSv2: 7.5) bypass authentication check via crafted values + +# key exchange algorithms +(kex) diffie-hellman-group-exchange-sha256 (1024-bit) -- [fail] using small 1024-bit modulus + `- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group-exchange-sha1 (1024-bit) -- [fail] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 +(kex) diffie-hellman-group1-sha1 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled (in client) since OpenSSH 7.0, logjam attack + `- [warn] using small 1024-bit modulus + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 + +# host-key algorithms +(key) ssh-rsa (3072-bit) -- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ssh-rsa-cert-v01@openssh.com (3072-bit cert/3072-bit CA) -- [info] available since OpenSSH 5.6 + +# encryption algorithms (ciphers) +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) arcfour256 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) arcfour128 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 4.2 +(enc) aes128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.28 +(enc) 3des-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.4, unsafe algorithm + `- [warn] using weak cipher + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) blowfish-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [fail] disabled since Dropbear SSH 0.53 + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 1.2.2, Dropbear SSH 0.28 +(enc) cast128-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [warn] using small 64-bit block size + `- [info] available since OpenSSH 2.1.0 +(enc) aes192-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 +(enc) aes256-cbc -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0, Dropbear SSH 0.47 +(enc) arcfour -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher + `- [info] available since OpenSSH 2.1.0 +(enc) rijndael-cbc@lysator.liu.se -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using weak cipher mode + `- [info] available since OpenSSH 2.3.0 + +# message authentication code algorithms +(mac) hmac-md5 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) hmac-ripemd160 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.5.0 +(mac) hmac-ripemd160@openssh.com -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 2.1.0 +(mac) hmac-sha1-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.47 +(mac) hmac-md5-96 -- [fail] removed (in server) since OpenSSH 6.7, unsafe algorithm + `- [warn] disabled (in client) since OpenSSH 7.2, legacy algorithm + `- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.5.0 + +# fingerprints +(fin) ssh-rsa: SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244 + +# algorithm recommendations (for OpenSSH 5.6) +(rec) !diffie-hellman-group-exchange-sha256 -- kex algorithm to change (increase modulus size to 2048 bits or larger)  +(rec) -3des-cbc -- enc algorithm to remove  +(rec) -aes128-cbc -- enc algorithm to remove  +(rec) -aes192-cbc -- enc algorithm to remove  +(rec) -aes256-cbc -- enc algorithm to remove  +(rec) -arcfour -- enc algorithm to remove  +(rec) -arcfour128 -- enc algorithm to remove  +(rec) -arcfour256 -- enc algorithm to remove  +(rec) -blowfish-cbc -- enc algorithm to remove  +(rec) -cast128-cbc -- enc algorithm to remove  +(rec) -diffie-hellman-group-exchange-sha1 -- kex algorithm to remove  +(rec) -diffie-hellman-group1-sha1 -- kex algorithm to remove  +(rec) -hmac-md5 -- mac algorithm to remove  +(rec) -hmac-md5-96 -- mac algorithm to remove  +(rec) -hmac-ripemd160 -- mac algorithm to remove  +(rec) -hmac-ripemd160@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha1-96 -- mac algorithm to remove  +(rec) -rijndael-cbc@lysator.liu.se -- enc algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_8.0p1_test1.txt b/test/docker/expected_results/openssh_8.0p1_test1.txt new file mode 100644 index 0000000..129f107 --- /dev/null +++ b/test/docker/expected_results/openssh_8.0p1_test1.txt @@ -0,0 +1,82 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_8.0 +(gen) software: OpenSSH 8.0 +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp384 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp521 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group16-sha512 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group18-sha512 -- [info] available since OpenSSH 7.3 +(kex) diffie-hellman-group14-sha256 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 + +# host-key algorithms +(key) rsa-sha2-512 (3072-bit) -- [info] available since OpenSSH 7.2 +(key) rsa-sha2-256 (3072-bit) -- [info] available since OpenSSH 7.2 +(key) ssh-rsa (3072-bit) -- [info] available since OpenSSH 2.5.0, Dropbear SSH 0.28 +(key) ecdsa-sha2-nistp256 -- [fail] using weak elliptic curves + `- [warn] using weak random number generator could reveal the key + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes128-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes256-gcm@openssh.com -- [info] available since OpenSSH 6.2 + +# message authentication code algorithms +(mac) umac-64-etm@openssh.com -- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 6.2 +(mac) umac-128-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-512-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha1-etm@openssh.com -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 6.2 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) umac-128@openssh.com -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha2-512 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# fingerprints +(fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU +(fin) ssh-rsa: SHA256:nsWtdJ9Z67Vrf7OsUzQov7esXhsWAfVppArGh25u244 + +# algorithm recommendations (for OpenSSH 8.0) +(rec) -ecdh-sha2-nistp256 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp384 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp521 -- kex algorithm to remove  +(rec) -ecdsa-sha2-nistp256 -- key algorithm to remove  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +(rec) -hmac-sha1 -- mac algorithm to remove  +(rec) -hmac-sha1-etm@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha2-256 -- mac algorithm to remove  +(rec) -hmac-sha2-512 -- mac algorithm to remove  +(rec) -umac-128@openssh.com -- mac algorithm to remove  +(rec) -umac-64-etm@openssh.com -- mac algorithm to remove  +(rec) -umac-64@openssh.com -- mac algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_8.0p1_test2.txt b/test/docker/expected_results/openssh_8.0p1_test2.txt new file mode 100644 index 0000000..bf35175 --- /dev/null +++ b/test/docker/expected_results/openssh_8.0p1_test2.txt @@ -0,0 +1,78 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_8.0 +(gen) software: OpenSSH 8.0 +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp256 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp384 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) ecdh-sha2-nistp521 -- [fail] using weak elliptic curves + `- [info] available since OpenSSH 5.7, Dropbear SSH 2013.62 +(kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4 +(kex) diffie-hellman-group16-sha512 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group18-sha512 -- [info] available since OpenSSH 7.3 +(kex) diffie-hellman-group14-sha256 -- [info] available since OpenSSH 7.3, Dropbear SSH 2016.73 +(kex) diffie-hellman-group14-sha1 -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 3.9, Dropbear SSH 0.53 + +# host-key algorithms +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 +(key) ssh-ed25519-cert-v01@openssh.com -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes128-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes256-gcm@openssh.com -- [info] available since OpenSSH 6.2 + +# message authentication code algorithms +(mac) umac-64-etm@openssh.com -- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 6.2 +(mac) umac-128-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-512-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha1-etm@openssh.com -- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 6.2 +(mac) umac-64@openssh.com -- [warn] using encrypt-and-MAC mode + `- [warn] using small 64-bit tag size + `- [info] available since OpenSSH 4.7 +(mac) umac-128@openssh.com -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha2-512 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 +(mac) hmac-sha1 -- [warn] using encrypt-and-MAC mode + `- [warn] using weak hashing algorithm + `- [info] available since OpenSSH 2.1.0, Dropbear SSH 0.28 + +# fingerprints +(fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU + +# algorithm recommendations (for OpenSSH 8.0) +(rec) -ecdh-sha2-nistp256 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp384 -- kex algorithm to remove  +(rec) -ecdh-sha2-nistp521 -- kex algorithm to remove  +(rec) +rsa-sha2-256 -- key algorithm to append  +(rec) +rsa-sha2-512 -- key algorithm to append  +(rec) +ssh-rsa -- key algorithm to append  +(rec) -diffie-hellman-group14-sha1 -- kex algorithm to remove  +(rec) -hmac-sha1 -- mac algorithm to remove  +(rec) -hmac-sha1-etm@openssh.com -- mac algorithm to remove  +(rec) -hmac-sha2-256 -- mac algorithm to remove  +(rec) -hmac-sha2-512 -- mac algorithm to remove  +(rec) -umac-128@openssh.com -- mac algorithm to remove  +(rec) -umac-64-etm@openssh.com -- mac algorithm to remove  +(rec) -umac-64@openssh.com -- mac algorithm to remove  + +# additional info +(nfo) For hardening guides on common OSes, please see:  + diff --git a/test/docker/expected_results/openssh_8.0p1_test3.txt b/test/docker/expected_results/openssh_8.0p1_test3.txt new file mode 100644 index 0000000..9a5bcc3 --- /dev/null +++ b/test/docker/expected_results/openssh_8.0p1_test3.txt @@ -0,0 +1,39 @@ +# general +(gen) banner: SSH-2.0-OpenSSH_8.0 +(gen) software: OpenSSH 8.0 +(gen) compatibility: OpenSSH 7.4+, Dropbear SSH 2018.76+ +(gen) compression: enabled (zlib@openssh.com) + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) diffie-hellman-group-exchange-sha256 (2048-bit) -- [info] available since OpenSSH 4.4 + +# host-key algorithms +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. +(enc) aes256-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes128-gcm@openssh.com -- [info] available since OpenSSH 6.2 +(enc) aes256-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 +(enc) aes192-ctr -- [info] available since OpenSSH 3.7 +(enc) aes128-ctr -- [info] available since OpenSSH 3.7, Dropbear SSH 0.52 + +# message authentication code algorithms +(mac) hmac-sha2-256-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) hmac-sha2-512-etm@openssh.com -- [info] available since OpenSSH 6.2 +(mac) umac-128-etm@openssh.com -- [info] available since OpenSSH 6.2 + +# fingerprints +(fin) ssh-ed25519: SHA256:UrnXIVH+7dlw8UqYocl48yUEcKrthGDQG2CPCgp7MxU + +# algorithm recommendations (for OpenSSH 8.0) +(rec) +diffie-hellman-group14-sha256 -- kex algorithm to append  +(rec) +diffie-hellman-group16-sha512 -- kex algorithm to append  +(rec) +diffie-hellman-group18-sha512 -- kex algorithm to append  +(rec) +rsa-sha2-256 -- key algorithm to append  +(rec) +rsa-sha2-512 -- key algorithm to append  +(rec) +ssh-rsa -- key algorithm to append  + diff --git a/test/docker/expected_results/tinyssh_20190101_test1.txt b/test/docker/expected_results/tinyssh_20190101_test1.txt new file mode 100644 index 0000000..26efda2 --- /dev/null +++ b/test/docker/expected_results/tinyssh_20190101_test1.txt @@ -0,0 +1,25 @@ +# general +(gen) software: TinySSH noversion +(gen) compatibility: OpenSSH 8.0+, Dropbear SSH 2018.76+ +(gen) compression: disabled + +# key exchange algorithms +(kex) curve25519-sha256 -- [info] available since OpenSSH 7.4, Dropbear SSH 2018.76 +(kex) curve25519-sha256@libssh.org -- [info] available since OpenSSH 6.5, Dropbear SSH 2013.62 +(kex) sntrup4591761x25519-sha512@tinyssh.org -- [warn] using experimental algorithm + `- [info] available since OpenSSH 8.0 + +# host-key algorithms +(key) ssh-ed25519 -- [info] available since OpenSSH 6.5 + +# encryption algorithms (ciphers) +(enc) chacha20-poly1305@openssh.com -- [info] available since OpenSSH 6.5 + `- [info] default cipher since OpenSSH 6.9. + +# message authentication code algorithms +(mac) hmac-sha2-256 -- [warn] using encrypt-and-MAC mode + `- [info] available since OpenSSH 5.9, Dropbear SSH 2013.56 + +# fingerprints +(fin) ssh-ed25519: SHA256:89ocln1x7KNqnMgWffGoYtD70ksJ4FrH7BMJHa7SrwU + diff --git a/test/docker/host_ca_ed25519 b/test/docker/host_ca_ed25519 new file mode 100644 index 0000000..7b8c41b --- /dev/null +++ b/test/docker/host_ca_ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQAAAKAa0zr8GtM6 +/AAAAAtzc2gtZWQyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQ +AAAEC/j/BpfmgaZqNMTkJXO4cKZBr31N5z33IRFjh5m6IDDhsz1andk9wLwh+G7oaM0Mlq +gyDsrE7R6Xb6v0nflOW1AAAAHWpkb2dAbG9jYWxob3N0LndvbmRlcmxhbmQubG9s +-----END OPENSSH PRIVATE KEY----- diff --git a/test/docker/host_ca_ed25519.pub b/test/docker/host_ca_ed25519.pub new file mode 100644 index 0000000..01e745f --- /dev/null +++ b/test/docker/host_ca_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBsz1andk9wLwh+G7oaM0MlqgyDsrE7R6Xb6v0nflOW1 jdog@localhost.wonderland.lol diff --git a/test/docker/host_ca_rsa_1024 b/test/docker/host_ca_rsa_1024 new file mode 100644 index 0000000..337b777 --- /dev/null +++ b/test/docker/host_ca_rsa_1024 @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS +6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8Su +dBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQIDAQAB +AoGBANALOUXRcP1tTtOP4+In/709dsONKyDBhPavGMFGsWtyIavBcbxU+bBzrq1j +3WJFCmi99xxAjjqMNInxhMgvSaoJtsiY0/FFxqRy6l/ZnRjI6hrVKR8whrPKVgBF +pvbjeQIn9txeCYA8kwl/Si762u7byq+qvupE53xMP94J02KBAkEA/Q4+Hn1Rjblw +VXynF+oXIq6iZy+8PW+Y/FIL8d31ehzfcssCMdFV6S3/wBoQkWby30oGC/xGmHGR +6ffXGilByQJBAOn3NMrBPXNkaPeQtgV3tk4s1dRDQYhbqGNz6tcgThyyPdhJCmCy +jgUEhLwAetsDI8/+3avWbo6/csOV+BvpYUkCQQDQyEp6L1z0+FV1QqY99dZmt/yn +89t0OLnZG/xc7osU1/OHq3TBE3y1KU2D+j1HKdAiZ9l7VAYOykzf46qmG/n5AkEA +2kWjfcjcIIw7lULvXZh6fuI7NwTr3V/Nb8MUA1EDLqhnJCG4SdAqyKmXf6Fe/HYo +cgKPIaIykIAxfCCsULXg6QJAOxB0CKYJlopVBdjGMlGqOEneWTmb1A2INQDE2Una +LkSd0Rr8OiEzDeemV7j3Ec4BH0HxGMnHDxMybZwoZRnRPw== +-----END RSA PRIVATE KEY----- diff --git a/test/docker/host_ca_rsa_1024.pub b/test/docker/host_ca_rsa_1024.pub new file mode 100644 index 0000000..6d861d6 --- /dev/null +++ b/test/docker/host_ca_rsa_1024.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8SudBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQ== jdog@localhost.wonderland.lol diff --git a/test/docker/host_ca_rsa_3072 b/test/docker/host_ca_rsa_3072 new file mode 100644 index 0000000..dd04653 --- /dev/null +++ b/test/docker/host_ca_rsa_3072 @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG4wIBAAKCAYEAqxQEIbj8w0TrBY1fDO81curijQrdLOUr8Vl8XECWc5QGd1Lk +AG80NgdcCBPvjWxZSmYrKeqA78GUdN+KgycE0ztpxYSXKHZMaIM5Xe94BB+BocH9 +1vd/2iBzGeed1nV/zfAdq2AEHQj1TpII+a+z25yxv2PuwVTTwwo9I/6JgNq3evH4 +Hbwgr3SRfEEYZQ+YL8cOpBuNg1YZOR0k1yk23ZqAd92JybxZ4iCtOt7rcj2sFHzN +u1U544wWBwIL5yZZKTgBhY4dqfT2Ep7IzR5HdsdrvQV9qC92GM1zDE+U3AwrVKjH +s0YZq3jzcq/yvFDCcMMRz4/0pGFFU26oWma+n3vbAxKJoL+rhG8QM9+l2qFlLGsn +M0kUXAJXsPKbygpaP8Z3U4eKgTuJ2GuS9eLIFnB7mrwD75V6GgN9q5mY89DfkVSk +HaoqpY8pPdRkz9QAmMEuLtHmv29CVOpfX5v/rsm7wASAZqtUlmFu4rFGBLwvZbUl +Wu02HmgBT47g6EIfAgMBAAECggGAKVCdKtO03yd+pomcodAHFWiaK7uq7FOwCAo3 +WUQT0Xe3FAwFmgFBF6cxV5YQ7RN0gN4poGbMmpoiUxNFLSU4KhcYFSZPJutiyn6e +VQwm7L/7G2hw+AAvdSsPAPuJh6g6pC5Py/pVI/ns2/uyhTIkem3eEz18BF6LAXgw +icfHx0GKu/tBk1TCg/zfwaUq0gUxGKC27XTl+QjK8JsUMY33fQ755Xiv9PMytcR0 +cVoyfBVewFffi1UqtMQ48ZpR65G743RxrP4/wcwsfD7n5LJLdyxQkh3gIMTJ8dd/ +R5V4FlueorRgjTbLTjGDxNrCAJ+locezhEEPXsPh2q0KiIXGyz2AMxaOqFmhU8oK +aVVt8pWJ+YsrKIgc/A3s18ezO8uO5ZdtjQ+CWguduUGY7YgWezGLO1LPxhJC4d7b +Q/xpeKveTRlcScAqOUzKgSuEhcvPgj8paUcRUoiXm4qiJBY5sXJks+YGp8BGksH0 +O94no+Ns2G58MlL+RyXk3JWrc6zRAoHBANdPplY2sIuIiiEBu95f1Qar1nCBHhB2 +i+HpnsUOdSlbxwMxoF8ffeN9N+DQqaqPu1RhFa5xbB2EUSujvOnL7b/RWqe1X9Po +UIt5UjXctNP/HYcQDyjXY+rV5SZhHDyv6TBYurNZlvlBivliDz82THPRtqVxed3B +w2MeaSkKAQ8rA7PE+0j3TG+YtIij0mHOhNPJgEZ/XZ9MIQOGMycRJhwOlclBI5NP +Ak6p30ArnU2fX4qMkU3i+wqUfXS1hhDihwKBwQDLaHWPIWPVbWdcCbYQTcUmFC3i +xkxd0UuLcfS9csk61nvdFj7m8tMExX+3fIo/fHEtzDd98Alc1i6/f6ePl0CX6NDu +QIWLryI1QQRQidHCdw0wQ3N3VD4ZXJHDeqBxogVAkA7A/1QeXwcXE/Xj2ZgyDwhL +3+myjmvWtw9zJsXL0F3tpPzn+Mrf0KRkWOaluOw7hMMjVjrgu6g24HMWbHHVLRTx +dlAI7tgxCAPe2SEi+1mzaVUZ8cfgqYqC3X66UakCgcEAopxtK7+yJi/A4pzEnnYS +FS/CjMV3R0fA7aXbW0hIBCxkaW0Zib3m/eCcSxZMjZxwBpIsJctTtBcylprbGlgB +/1TF+tNoxEo4Sp4eEL/XciTC0Da4vEewFrPklM/S26KfovvgRYPsGeP+aco9aahA +pVhFcT36pBiq0DkvgucjValO6n5iqgDboYzbDDdttKCcgLc2Qgf/VUfRxy+bgm3Z +MmdxiMXBcIfDXlW9XmGSNAWhyqnPM9uxbZQoC/Tsg+QRAoHANHMcFSsz9f2+8DGk +27FiC76aUmZ1nJ9yTmO1CwDFOMHDsK+iyqSEmy9eDm8zqsko2flVuciicWjdJw4A +o/sJceJbtYO3q9weAwNf3HCdQPq30OEjrfpwBNQk1fYR1xtDJXHADC4Kf8ZbKq0/ +81/Rad8McZwsQ5mL3xLXDgdKa5KwFa48dIhnr6y6JxHxb3wule5W7w62Ierhpjzc +EEUoWSLFyrmKS7Ni1cnOTbFJZR7Q831Or2Dz/E9bYwFAQ0T5AoHAM4/zU+8rsbdD +FvvhWsj7Ivfh6pxx1Tl1Wccaauea9AJayHht0FOzkycpJrH1E+6F5MzhkFFU1SUY +60NZxzSZgbU0HBrJRcRFyo510iMcnctdTdyh8p7nweGoD0oqXzf6cHqrUep8Y8rQ +gkSVhPE31+NGlPbwz+NOflcaaAWYiDC6wjVt1asaZq292SJD4DF1fAUkbQ2hxgyQ ++G/6y5ovrcGnh7q63RLhW1TRf8dD2D2Av9UgXDmWZAZ5n838FS+X +-----END RSA PRIVATE KEY----- diff --git a/test/docker/host_ca_rsa_3072.pub b/test/docker/host_ca_rsa_3072.pub new file mode 100644 index 0000000..b728ed7 --- /dev/null +++ b/test/docker/host_ca_rsa_3072.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCrFAQhuPzDROsFjV8M7zVy6uKNCt0s5SvxWXxcQJZzlAZ3UuQAbzQ2B1wIE++NbFlKZisp6oDvwZR034qDJwTTO2nFhJcodkxogzld73gEH4Ghwf3W93/aIHMZ553WdX/N8B2rYAQdCPVOkgj5r7PbnLG/Y+7BVNPDCj0j/omA2rd68fgdvCCvdJF8QRhlD5gvxw6kG42DVhk5HSTXKTbdmoB33YnJvFniIK063utyPawUfM27VTnjjBYHAgvnJlkpOAGFjh2p9PYSnsjNHkd2x2u9BX2oL3YYzXMMT5TcDCtUqMezRhmrePNyr/K8UMJwwxHPj/SkYUVTbqhaZr6fe9sDEomgv6uEbxAz36XaoWUsayczSRRcAlew8pvKClo/xndTh4qBO4nYa5L14sgWcHuavAPvlXoaA32rmZjz0N+RVKQdqiqljyk91GTP1ACYwS4u0ea/b0JU6l9fm/+uybvABIBmq1SWYW7isUYEvC9ltSVa7TYeaAFPjuDoQh8= jdog@localhost.wonderland.lol diff --git a/test/docker/moduli_1024 b/test/docker/moduli_1024 new file mode 100644 index 0000000..bd81dae --- /dev/null +++ b/test/docker/moduli_1024 @@ -0,0 +1,44 @@ +20190821035337 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08BE313B +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08C0B443 +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08D1AF8B +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08E76DDB +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08E8F5D3 +20190821035338 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08EE3F1B +20190821035338 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08F28387 +20190821035339 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC08F69A57 +20190821035339 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0903B157 +20190821035339 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0905C973 +20190821035339 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0909BCD3 +20190821035339 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC090F4A2B +20190821035340 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0933BC13 +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09395757 +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC093F40D7 +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09478D4F +20190821035340 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0953A4D7 +20190821035340 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC095B5C7B +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09696573 +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC096BA243 +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC096F3903 +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09850E4B +20190821035341 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC098A1C23 +20190821035341 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC098E08E7 +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09A4FF7F +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09AE4707 +20190821035342 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09B4CE73 +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09C60C6F +20190821035342 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC09D2588F +20190821035343 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A025067 +20190821035343 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A0E38EB +20190821035343 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A213923 +20190821035344 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A390CA7 +20190821035344 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A3C7ADB +20190821035344 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A44D497 +20190821035344 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A479B13 +20190821035345 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A5EF01F +20190821035345 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A615D43 +20190821035345 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A6BEADB +20190821035345 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A86309F +20190821035345 2 6 100 1023 5 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0A991E8F +20190821035346 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0AA32C53 +20190821035346 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0AA9FAAB +20190821035346 2 6 100 1023 2 F0B5E9E385A451D4F46BD2E354B5FCAAC21CA960E5D3D11F877DD50541ED125161E4A5055D528D67E525115BBFAB0B2A4AB8CF5BA98A8BBA41803ED5D4CF766E9ECD39A8D8D914B6F346E0EB2BA6936082751676DCE5C4817EFC7A8105C2A094B22C25245BE13CA4085F2985D3B7A2636FF4018A7E4EA9840BF5FFBC0AAC42BB diff --git a/test/docker/ssh1_host_key b/test/docker/ssh1_host_key new file mode 100644 index 0000000..c98971c Binary files /dev/null and b/test/docker/ssh1_host_key differ diff --git a/test/docker/ssh1_host_key.pub b/test/docker/ssh1_host_key.pub new file mode 100644 index 0000000..b66c66f --- /dev/null +++ b/test/docker/ssh1_host_key.pub @@ -0,0 +1 @@ +1024 35 150823875409720459951648542224727752099073441604930026287525797402159071426070997897033651155038337251362080634963146983947007228274330777134724953282680928153520263171933106732090266742784258910450489054624715996015082463159338507115031336180486071622718809324273851629938883104520608180885444242395900180011 root@ubuntu1604server diff --git a/test/docker/ssh_host_dsa_key b/test/docker/ssh_host_dsa_key new file mode 100644 index 0000000..ecd47f9 --- /dev/null +++ b/test/docker/ssh_host_dsa_key @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBugIBAAKBgQDth1eV+A8j191R0ey0dVXL2LGNGYM+a+PomSa7suK8xNCeVLKC +YpQ6VSWpAf6FbRWev1UVo8IpbglwFZPcyFPK2G1H7p45ows2SN4CleszDD56e6W0 +3Plc+qMqSJ6LTjr4M5+HqTDOM3CS72d7MXUkfHQiagyrWQhXyc0kFsNJLwIVAKg7 +b5+NiIZzpg5IEH0tlYFQpuhBAoGAGcbq79QqNNZRuPCE/F05sCoTRGCmFnDjCuCg +WN7wNRotjMz/S3pHtCCeuTT1jT6Hy0ZFHftv0t/GF8GBRgeokUbS4ytHpOkFWcTz +8oFguDL44nq8eNfSY6bzEl84qsgEe4HP93mB4FR1ZUUgI4b7gCBOYEFl3yPiH7H1 +p7Z9E1oCgYAl1UPQkeRhElz+AgEbNsnMKu1+6O3/z95D1Wvv4OEwAImbytlBaC7p +kwJElJNsMMfGqCC8OHdJ0e4VQQUwk/GOhD0MFhVQHBtVZYbiWmVkpfHf1ouUQg3f +1IZmz2SSt6cPPEu+BEQ/Sn3mFRJ5XSTHLtnI0HJeDND5u1+6p1nXawIURv3Maige +oxmfqC24VoROJEq+sew= +-----END DSA PRIVATE KEY----- diff --git a/test/docker/ssh_host_dsa_key.pub b/test/docker/ssh_host_dsa_key.pub new file mode 100644 index 0000000..a32a5a0 --- /dev/null +++ b/test/docker/ssh_host_dsa_key.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAO2HV5X4DyPX3VHR7LR1VcvYsY0Zgz5r4+iZJruy4rzE0J5UsoJilDpVJakB/oVtFZ6/VRWjwiluCXAVk9zIU8rYbUfunjmjCzZI3gKV6zMMPnp7pbTc+Vz6oypInotOOvgzn4epMM4zcJLvZ3sxdSR8dCJqDKtZCFfJzSQWw0kvAAAAFQCoO2+fjYiGc6YOSBB9LZWBUKboQQAAAIAZxurv1Co01lG48IT8XTmwKhNEYKYWcOMK4KBY3vA1Gi2MzP9Leke0IJ65NPWNPofLRkUd+2/S38YXwYFGB6iRRtLjK0ek6QVZxPPygWC4Mvjierx419JjpvMSXziqyAR7gc/3eYHgVHVlRSAjhvuAIE5gQWXfI+IfsfWntn0TWgAAAIAl1UPQkeRhElz+AgEbNsnMKu1+6O3/z95D1Wvv4OEwAImbytlBaC7pkwJElJNsMMfGqCC8OHdJ0e4VQQUwk/GOhD0MFhVQHBtVZYbiWmVkpfHf1ouUQg3f1IZmz2SSt6cPPEu+BEQ/Sn3mFRJ5XSTHLtnI0HJeDND5u1+6p1nXaw== diff --git a/test/docker/ssh_host_ecdsa_key b/test/docker/ssh_host_ecdsa_key new file mode 100644 index 0000000..69eea7b --- /dev/null +++ b/test/docker/ssh_host_ecdsa_key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICq/YV5QenL0uW5g5tCjY3EWs+UBFmskY+Jjt2vd2aEmoAoGCCqGSM49 +AwEHoUQDQgAEdYSxDVUjOpW479L/nRDiAdxRB5Kuy2bgkP/LA2pnWPcGIWmFa4QU +YN2U3JsFKcLIcx5cvTehQfgrHDnaSKVdKA== +-----END EC PRIVATE KEY----- diff --git a/test/docker/ssh_host_ecdsa_key.pub b/test/docker/ssh_host_ecdsa_key.pub new file mode 100644 index 0000000..4e17058 --- /dev/null +++ b/test/docker/ssh_host_ecdsa_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHWEsQ1VIzqVuO/S/50Q4gHcUQeSrstm4JD/ywNqZ1j3BiFphWuEFGDdlNybBSnCyHMeXL03oUH4Kxw52kilXSg= diff --git a/test/docker/ssh_host_ed25519_key b/test/docker/ssh_host_ed25519_key new file mode 100644 index 0000000..3388574 --- /dev/null +++ b/test/docker/ssh_host_ed25519_key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACC/9RD2Ao95ODDIH8i11ekTALut8AUNqWoQx0jHlP4xygAAAKDiqVOs4qlT +rAAAAAtzc2gtZWQyNTUxOQAAACC/9RD2Ao95ODDIH8i11ekTALut8AUNqWoQx0jHlP4xyg +AAAECTmHGkq0Qea0QqTJYMXL0bpxVU7mhgwYninfVWxrA017/1EPYCj3k4MMgfyLXV6RMA +u63wBQ2pahDHSMeU/jHKAAAAHWpkb2dAbG9jYWxob3N0LndvbmRlcmxhbmQubG9s +-----END OPENSSH PRIVATE KEY----- diff --git a/test/docker/ssh_host_ed25519_key-cert.pub b/test/docker/ssh_host_ed25519_key-cert.pub new file mode 100644 index 0000000..8eef563 --- /dev/null +++ b/test/docker/ssh_host_ed25519_key-cert.pub @@ -0,0 +1 @@ +ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAIO1W0I8tD0c4LypvHY1XNch3BQCw9Yy28/4KmAYql80DAAAAIL/1EPYCj3k4MMgfyLXV6RMAu63wBQ2pahDHSMeU/jHKAAAAAAAAAAAAAAACAAAABHRlc3QAAAAIAAAABHRlc3QAAAAAXV7hvAAAAACBa2YhAAAAAAAAAAAAAAAAAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAbM9Wp3ZPcC8Ifhu6GjNDJaoMg7KxO0el2+r9J35TltQAAAFMAAAALc3NoLWVkMjU1MTkAAABAW60bCSeIG4Ta+57zgkSbW4LIGCxtOuJJ+pP3i3S0xJJfHGnOtXbg0NQm7pulNl/wd01kgJO9A7RjbhTh7TV1AA== ssh_host_ed25519_key.pub diff --git a/test/docker/ssh_host_ed25519_key.pub b/test/docker/ssh_host_ed25519_key.pub new file mode 100644 index 0000000..e56a56a --- /dev/null +++ b/test/docker/ssh_host_ed25519_key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL/1EPYCj3k4MMgfyLXV6RMAu63wBQ2pahDHSMeU/jHK diff --git a/test/docker/ssh_host_rsa_key_1024 b/test/docker/ssh_host_rsa_key_1024 new file mode 100644 index 0000000..e9023b6 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024 @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ4 +0toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6J +WHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQIDAQAB +AoGATGZ16s5NqDsWJ4B9k3xx/2wZZ+BGzl6a7D0habq97XLn8HGoK6UqTBFk6lnO +WSy0hZBPrNq0AzqCDJY7RrfuZqgVAu/+HEFuXencgt8Z//ueYBaGK8yAC+OrMnDG +LbSoIGRq8saaFtCzt47c+uSVsrhJ4TvK5gbceZuD/2uw10ECQQD79T0j+YWsLISK +PKvYHqEXSMPN6b+lK9hRPLoF9NMksNLSjuxxhkYHz+hJPVNT+wPtRMAYmMdPXfKa +FjuErXVFAkEA4ZgJIOeJ7OHfqGEgd29m36yFy0UaUJ+cmYuJzHAYWgW3TOanqpZm +A8EENuXvH0DtYRVytv4m/cIRVVPxWtXzsQJBALXlQUOEc0VuSi1GScVXr3KQ3JL+ +ipWixqM3VRDRw9D8Ouc5uWbnygz/wrGFLXA2ioozlP7s5Q7eQzOMk2FgnIUCQQCz +j5QUgLcjuVWQbF6vMhisCGImPUaIzcKT5KE1/DMl1E7mAuGJwlRIwKVeHP6L3d4T +3EKGrRzT9lhdlocRSiLBAkEAi3xI0MOZp4xGviPc1C1TKuqdJSr8fHwbtLozwNQO +nnF6m5S72JzZEThDBZS9zcdBp9EFpTvUGzx/O0GI454eoA== +-----END RSA PRIVATE KEY----- diff --git a/test/docker/ssh_host_rsa_key_1024-cert_1024.pub b/test/docker/ssh_host_rsa_key_1024-cert_1024.pub new file mode 100644 index 0000000..17c7738 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024-cert_1024.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgFqxXSa9HTqCw5YW3DdIwVREPGxI+i56w32RnHWRg0NoAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQAAAAAAAAAAAAAAAgAAAAR0ZXN0AAAACAAAAAR0ZXN0AAAAAF1evHgAAAAAgWtAtQAAAAAAAAAAAAAAAAAAAJcAAAAHc3NoLXJzYQAAAAMBAAEAAACBAOdGU3cAWdR7aUV/lcb1SFcuv/086u41MVsy3TOuM5RKOYBLuFLqkO/lUROhPoNpHURBRhqpIykdjNmG4Irna+vJ06blcVsnvvXav0zJlBSGhVnHxK50EfNUMhU7eNwwxYiWt9YEydRpQcSqmbLzxjuAoNrZNbEmDX6GDnUqoetRAAAAjwAAAAdzc2gtcnNhAAAAgFRc2g0eWXGmqSa6Z8rcMPHf4rNMornEHSnTzZ8Rdh4YBhDa9xMRRy6puaWPDzXxOfZh7eSjCwrzUMEXTgCIO4oX62xm/6kUnAmKhXle0+inR/hdPg03daE0SBJ4spBT49lJ4WIW38RKFNzjmYg70rTPAnP8oM/F3CC1GV117/Vv ssh_host_rsa_key_1024.pub diff --git a/test/docker/ssh_host_rsa_key_1024-cert_3072.pub b/test/docker/ssh_host_rsa_key_1024-cert_3072.pub new file mode 100644 index 0000000..ea0160a --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024-cert_3072.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgrJGcfyW8V6VWGT7lD1ardj2RtTP8TOjmLRNbuoGkyZQAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQAAAAAAAAAAAAAAAgAAAAR0ZXN0AAAACAAAAAR0ZXN0AAAAAF1evHgAAAAAgWtA7gAAAAAAAAAAAAAAAAAAAZcAAAAHc3NoLXJzYQAAAAMBAAEAAAGBAKsUBCG4/MNE6wWNXwzvNXLq4o0K3SzlK/FZfFxAlnOUBndS5ABvNDYHXAgT741sWUpmKynqgO/BlHTfioMnBNM7acWElyh2TGiDOV3veAQfgaHB/db3f9ogcxnnndZ1f83wHatgBB0I9U6SCPmvs9ucsb9j7sFU08MKPSP+iYDat3rx+B28IK90kXxBGGUPmC/HDqQbjYNWGTkdJNcpNt2agHfdicm8WeIgrTre63I9rBR8zbtVOeOMFgcCC+cmWSk4AYWOHan09hKeyM0eR3bHa70FfagvdhjNcwxPlNwMK1Sox7NGGat483Kv8rxQwnDDEc+P9KRhRVNuqFpmvp972wMSiaC/q4RvEDPfpdqhZSxrJzNJFFwCV7Dym8oKWj/Gd1OHioE7idhrkvXiyBZwe5q8A++VehoDfauZmPPQ35FUpB2qKqWPKT3UZM/UAJjBLi7R5r9vQlTqX1+b/67Ju8AEgGarVJZhbuKxRgS8L2W1JVrtNh5oAU+O4OhCHwAAAY8AAAAHc3NoLXJzYQAAAYCO78ONpHp+EGWDqDLb/GPFDH32o6oaRRrIH/Bhvg1FOi5XngnHTdU7xWdnJqNE2Sl6VOrg0sTCMYcX9fZ8tVREnCo3aF7Iwow5Br67QYKayRzHANQqHaVK46lpI1gz81V00u54tX1F8oEUqm6sRmFKFuklt6CjfbR+tnpj7DrfeOTKEBOGJP2uU0jMsJr2DrBeXrzONjIJtIJ1AxWjXd2LeIWO2C6yTkcN5ggThMMaeu6QuuBPpC2PN2COfu+Mgto9g103+/SS4Wa8CzinZZn2Xe1isUxI8QRNrShy4Hl/bIZQL7mi/0rxkfw+fA7IzMk462V99gPVSp+/jK0sbUJoC3QeeglS5hWodjW+VGZfgweGQ+AE/OxkNSv+kDPMYEkjfOf4qhxS5QFvButLt6zp2UNbE5+OWvYpdjO9/DOa0ro+wCw07+dVKIcDpU2csiCcJvQ/HmKAmhch7jOHa0WaxSX0tt0xTPJWTvr6E4WZOgEnk9AvWmrKjF5tEzGYwTU= ssh_host_rsa_key_1024.pub diff --git a/test/docker/ssh_host_rsa_key_1024.pub b/test/docker/ssh_host_rsa_key_1024.pub new file mode 100644 index 0000000..1da6065 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_1024.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDeCC1U7VqVg9AfrfWrXACiW6pzYOuP8tim68z+YN/dUU7JhFZ40toteQkLcJBAD2miQ6ZJYkjVfhQ4FRFeOW5vcN0UYHn8ttb2mKdGJdt24ZYY5Z6JWHQhPOpSgtWyUv6RnxU2ligEeaoPaiepUUOhoyLf4WcF7voVCAKZNqeTtQ== diff --git a/test/docker/ssh_host_rsa_key_3072 b/test/docker/ssh_host_rsa_key_3072 new file mode 100644 index 0000000..3a2c719 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072 @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIG5AIBAAKCAYEAzhHp7eFnrQAJqOd7aihyQyIDKgxCF7H51Q3Ft3+8af+lX3Ol +Ie77Gi5GNNM+eRB4OzG+CBslxN5I3pM//sZ+gyylA1VuWZZkOlgtbOHutIkO2ldk +XtoGidla0VAxLcUcUK6cCmqwBTT31Hp4Qimp2zyeg/l5q0DhWKguY13lrm5b3YZY +rj7CW3Ktzxf8SbYz6du8KF0dHCWilzq+FLeGzXr7Yul5njVF5njkGvZ9duQ0qiVR +zqZkrkLEWgQlCM0T+PyUbvedL1MfDZPHGh7ZhU0snOvJRsxAr31tlknq+WwauZYd +DzJf1g1URcM65UwEsPlfgOW3ZoZogR1v57Im+KdsKhq2B3snEtJgdQh06JyO0ur4 +uUXo1mMtvBFhiptUtwP4g9v/IN4neeK+wBRom46m2Q1bMUBPneBOa8r2SY/3ynrz +XuVIWFOQtF60aJ+BNqvgUVCKOmz1KzoJwTqGm+EFaKM5z+UQWjIbSE3Ge4X5hXtk +Ou52v+tyDUk6boZLAgMBAAECggGAdrhxWmA7N7tG1W2Pd6iXs7+brRTk2vvpYGqP +11kbNsJXBzf8EiG5vuqb/gEaA+uOKSRORCNHzjT2LG0POHwpFO+aneIRMkHnuolk +mk9ME+zGhtpEdDUOAUsc/GxD+QePeZgvQ/0VLdrHUT3BnPSd7DXvaT9IbnZxnX8/ +QnYtRiJEgMrOuoxjswXNxvsdmWYEYJ38uBB1Hes80f3A1vSpECbjP6gdLh2pCM/r +MvGBdQaipMfdar4IUTEcKHQs1fY3mlAxnWRjYCqJPmq10d3NrdUrHb2zBE1HCC4h +aj2ycTxFhDJqGV6Y2AboHqh2c7lPJ+R2UjI9mIpALZSviHB1POcpWCAGA3NKjri9 +8jgxl3bj03ikJNfCuvlqRTa8at63W2zZTMRsxamoiO023uUOEMNBPwWXP/rVhQ8g +ufih0SY44j0EMPIuu2PoQV4ZSOtDw8xdPrchVCa078/pP5cRa4uV0bl2K4as+cYC +BhjEq2Org3ulDW2n6Mz5ZS7NbAkxAoHBAP/bgPGKX7rfrHa5MRHIgbLSmZtUoF51 +YGelc8ytRx6UT6wriJ1jQRXiI5mZlIXyVxMpIz9s4+h59kF+LpZuNLc3vTYpPOQn +RUDBVY6+SPC5MancL7bfBoHahpWEJuJB/WUE7eWvQM03/LsBtU6Nq+R632t5KdqF +A4y86qgD1vIjcBWvySLFJZGOCoNbj7ZinoBUO3ueYK6SUj8xH6TAqOJsTPvquRT3 +AFBpFBmrVc24wW7wTiLkQOhkIQs1J/ZhYwKBwQDOL07qF8wsoQBBTTXkZ59BCauz +R8kfqe5oUBwsmGJdiIHX6gutBA07sSwzVekIvCCkJFXk3TxLoBSMHEZEIdnS+HVt +gMIacYuhbh+XztdY0kadH/SMbVQD/2LZcL99vcZPq1QF3cHb0Buip5+fyAYjoEc7 +oVgvewD/TwdNcMjos/kMNh6l04kLi6vQG3WhoSBPWaoB669ppBNXSrWKe43nXVi6 +EvjGEiL+HCCnmD6LiD6p797Owu9AChP6fXInD/kCgcEAiLP3SRbt3yLzOtvn4+CF +q83qVJv6s31zbO1x2cIbZbNIfm0kKTOG6vJQoxjzyj2ZWJt6QcEkZGoFsSiCK83m +TJ5zciTGbACvd9HUrNfukO/iISeMNuEi0O65Sdm6DNnFUdw4X6grr3pihmh7PuVj +GkisZvft7Nt08hVeKzch+W4FzRCHHxTG5eZGp7icKI64sUhQH9SXQ67aUvkkNxrZ +IWFMIK1hBlqSyGPcYXqx9aDpeSTcGrhqFcCqBxr3pySRAoHAfJNO3delEC3yxoHN +FwSYzyX1rOuplE0K89G7RCKKBDNPKFKL3Wx+Rluk9htpIlLwcdxWXWJiZNsCrykC +N3YwcuyVnqTWIj4KfG3Z/tIFgPADpDnDevkvcv7iDbi2qlV4NXix2p2C3LnfiKY4 +psSnGO1lPJ0eeAmcr6VjJyIG8bqTthIY8F5gBi7Mj3+X0iFVMTxeoKxzHqP435wP +Fe3S7kCTNFH0J1Cb/eamwDwXRhz6p5h7iXd0MMAmFAmpZ/qZAoHBAPDSIvk2ocf1 +FVW8pKtKOJFIs8iQVIaOLKwPJVP8/JsB1+7mQx5KMoROb5pNpX2edN4vvG0CgqpJ +KekleqpH6nQCqYGFZ1BDhORElNILxeJHcNl0eAG++IJ2PfIpTZV30edDqMm0x7EI +8POZWAx809VzcYbE2jsgpN/EuiaG30EAI5yNvyzmZRCyQykH+eltHlCx17MWBxRQ +bb2UUfpdInTMS2vyrvkeUACkC1DGYdBVVBqqPTkHZg+Kcbs8ntQqEQ== +-----END RSA PRIVATE KEY----- diff --git a/test/docker/ssh_host_rsa_key_3072-cert_1024.pub b/test/docker/ssh_host_rsa_key_3072-cert_1024.pub new file mode 100644 index 0000000..da6b9ec --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072-cert_1024.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgGHz1JwF/1IcxW3pdQtpqbUjIHaFuk0cR/+l50vG+9hIAAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhksAAAAAAAAAAAAAAAIAAAAEdGVzdAAAAAgAAAAEdGVzdAAAAABdXry0AAAAAIFrQR8AAAAAAAAAAAAAAAAAAACXAAAAB3NzaC1yc2EAAAADAQABAAAAgQDnRlN3AFnUe2lFf5XG9UhXLr/9POruNTFbMt0zrjOUSjmAS7hS6pDv5VEToT6DaR1EQUYaqSMpHYzZhuCK52vrydOm5XFbJ7712r9MyZQUhoVZx8SudBHzVDIVO3jcMMWIlrfWBMnUaUHEqpmy88Y7gKDa2TWxJg1+hg51KqHrUQAAAI8AAAAHc3NoLXJzYQAAAIB4HaEexgQ9T6rScEbiHZx+suCaYXI7ywLYyoSEO48K8o+MmO83UTLtpPa3DXlT8hSYL8Aq6Bb5AMkDawsgsC484owPqObT/5ndLG/fctNBFcCTSL0ftte+A8xH0pZaGRoKbdxxgMqX4ubrCXpbMLGF9aAeh7MRa756XzqGlsCiSA== ssh_host_rsa_key_3072.pub diff --git a/test/docker/ssh_host_rsa_key_3072-cert_3072.pub b/test/docker/ssh_host_rsa_key_3072-cert_3072.pub new file mode 100644 index 0000000..78f49ea --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072-cert_3072.pub @@ -0,0 +1 @@ +ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg9MVX4OlkEy3p9eC+JJp8h7j76EmI46EY/RXxCGSWTC0AAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhksAAAAAAAAAAAAAAAIAAAAEdGVzdAAAAAgAAAAEdGVzdAAAAABdXr0sAAAAAIFrQWwAAAAAAAAAAAAAAAAAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCrFAQhuPzDROsFjV8M7zVy6uKNCt0s5SvxWXxcQJZzlAZ3UuQAbzQ2B1wIE++NbFlKZisp6oDvwZR034qDJwTTO2nFhJcodkxogzld73gEH4Ghwf3W93/aIHMZ553WdX/N8B2rYAQdCPVOkgj5r7PbnLG/Y+7BVNPDCj0j/omA2rd68fgdvCCvdJF8QRhlD5gvxw6kG42DVhk5HSTXKTbdmoB33YnJvFniIK063utyPawUfM27VTnjjBYHAgvnJlkpOAGFjh2p9PYSnsjNHkd2x2u9BX2oL3YYzXMMT5TcDCtUqMezRhmrePNyr/K8UMJwwxHPj/SkYUVTbqhaZr6fe9sDEomgv6uEbxAz36XaoWUsayczSRRcAlew8pvKClo/xndTh4qBO4nYa5L14sgWcHuavAPvlXoaA32rmZjz0N+RVKQdqiqljyk91GTP1ACYwS4u0ea/b0JU6l9fm/+uybvABIBmq1SWYW7isUYEvC9ltSVa7TYeaAFPjuDoQh8AAAGPAAAAB3NzaC1yc2EAAAGAG8tCiBMSq3Of3Gmcrid2IfPmaaemYivgEEuK8ubq1rznF0vtR07/NUQ7WVzfJhUSeG0gtJ3A1ey60NjcBn0DHao4Q3ATIXnkSOIKjNolZ2urqYv9fT1LAC4I5XWGzK2aKK0NEqAYr06YPtcGOBQk5+3GPAWSJ4eQycKRz5BSuMYbKaVxU0kGSvbavG07ZntMQhia/lILyq84PjXh/JlRVpIqY+LAS0qwqkUR3gWMTmvYvYI7fXU84ReVB1ut75bY7Xx0DXHPl1Zc2MNDLGcKsByZtoO9ueZRyOlZMUJcVP5fK+OUuZKjMbCaaJnV55BQ78/rftIPYsTEEO2Sf9WT86ADa3k4S0pyWqlTxBzZcDWNt+fZFNm9wcqcYS32nDKtfixcDN8E/IJIWY7aoabPqoYnKUVQBOcIEnZf1HqsKUVmF44Dp9mKhefUs3BtcdK63j/lNXzzMrPwZQAreJqH/uV3TgYBLjMPl++ctX6tCe6Hv5zFKNhnOCSBSzcsCgIU ssh_host_rsa_key_3072.pub diff --git a/test/docker/ssh_host_rsa_key_3072.pub b/test/docker/ssh_host_rsa_key_3072.pub new file mode 100644 index 0000000..ad83cd1 --- /dev/null +++ b/test/docker/ssh_host_rsa_key_3072.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDOEent4WetAAmo53tqKHJDIgMqDEIXsfnVDcW3f7xp/6Vfc6Uh7vsaLkY00z55EHg7Mb4IGyXE3kjekz/+xn6DLKUDVW5ZlmQ6WC1s4e60iQ7aV2Re2gaJ2VrRUDEtxRxQrpwKarAFNPfUenhCKanbPJ6D+XmrQOFYqC5jXeWublvdhliuPsJbcq3PF/xJtjPp27woXR0cJaKXOr4Ut4bNevti6XmeNUXmeOQa9n125DSqJVHOpmSuQsRaBCUIzRP4/JRu950vUx8Nk8caHtmFTSyc68lGzECvfW2WSer5bBq5lh0PMl/WDVRFwzrlTASw+V+A5bdmhmiBHW/nsib4p2wqGrYHeycS0mB1CHTonI7S6vi5RejWYy28EWGKm1S3A/iD2/8g3id54r7AFGibjqbZDVsxQE+d4E5ryvZJj/fKevNe5UhYU5C0XrRon4E2q+BRUIo6bPUrOgnBOoab4QVooznP5RBaMhtITcZ7hfmFe2Q67na/63INSTpuhks= diff --git a/test/mypy-py2.sh b/test/mypy-py2.sh deleted file mode 100755 index f8e9244..0000000 --- a/test/mypy-py2.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type mypy > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: mypy (Optional Static Typing for Python) not found." - exit 1 -fi -_htmldir="${_cdir}/../html/mypy-py2" -mkdir -p "${_htmldir}" -mypy --python-version 2.7 --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy-py3.sh b/test/mypy-py3.sh deleted file mode 100755 index 0d2dfe5..0000000 --- a/test/mypy-py3.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type mypy > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: mypy (Optional Static Typing for Python) not found." - exit 1 -fi -_htmldir="${_cdir}/../html/mypy-py3" -mkdir -p "${_htmldir}" -mypy --python-version 3.5 --config-file "${_cdir}/mypy.ini" --html-report "${_htmldir}" "${_cdir}/../ssh-audit.py" diff --git a/test/mypy.ini b/test/mypy.ini deleted file mode 100644 index 9c0a3e0..0000000 --- a/test/mypy.ini +++ /dev/null @@ -1,9 +0,0 @@ -[mypy] -silent_imports = True -disallow_untyped_calls = True -disallow_untyped_defs = True -check_untyped_defs = True -disallow-subclassing-any = True -warn-incomplete-stub = True -warn-redundant-casts = True - diff --git a/test/prospector.sh b/test/prospector.sh deleted file mode 100755 index 4398ec7..0000000 --- a/test/prospector.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -_cdir=$(cd -- "$(dirname "$0")" && pwd) -type prospector > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "err: prospector (Python Static Analysis) not found." - exit 1 -fi -if [ X"$1" == X"" ]; then - _file="${_cdir}/../ssh-audit.py" -else - _file="$1" -fi -prospector -E --profile-path "${_cdir}" -P prospector "${_file}" diff --git a/test/prospector.yml b/test/prospector.yml deleted file mode 100644 index 474af15..0000000 --- a/test/prospector.yml +++ /dev/null @@ -1,42 +0,0 @@ -strictness: veryhigh -doc-warnings: false - -pylint: - disable: - - multiple-imports - - invalid-name - - trailing-whitespace - - options: - max-args: 8 # default: 5 - max-locals: 20 # default: 15 - max-returns: 6 - max-branches: 15 # default: 12 - max-statements: 60 # default: 50 - max-parents: 7 - max-attributes: 8 # default: 7 - min-public-methods: 1 # default: 2 - max-public-methods: 20 - max-bool-expr: 5 - max-nested-blocks: 6 # default: 5 - max-line-length: 80 # default: 100 - ignore-long-lines: ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?)$ - max-module-lines: 2500 # default: 10000 - -pep8: - disable: - - W191 # indentation contains tabs - - W293 # blank line contains whitespace - - E101 # indentation contains mixed spaces and tabs - - E401 # multiple imports on one line - - E501 # line too long - - E221 # multiple spaces before operator - -pyflakes: - disable: - - F401 # module imported but unused - - F821 # undefined name - -mccabe: - options: - max-complexity: 15 diff --git a/test/stubs/colorama.pyi b/test/stubs/colorama.pyi new file mode 100644 index 0000000..81d6ef0 --- /dev/null +++ b/test/stubs/colorama.pyi @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from typing import Optional + +def init(autoreset: bool = False, convert: Optional[bool] = None, strip: Optional[bool] = None, wrap: bool = True) -> None: ... + diff --git a/test/test_auditconf.py b/test/test_auditconf.py index 3472c42..a901299 100644 --- a/test/test_auditconf.py +++ b/test/test_auditconf.py @@ -10,8 +10,8 @@ class TestAuditConf(object): self.AuditConf = ssh_audit.AuditConf self.usage = ssh_audit.usage - @classmethod - def _test_conf(cls, conf, **kwargs): + @staticmethod + def _test_conf(conf, **kwargs): options = { 'host': None, 'port': 22, @@ -20,7 +20,7 @@ class TestAuditConf(object): 'batch': False, 'colors': True, 'verbose': False, - 'minlevel': 'info', + 'level': 'info', 'ipv4': True, 'ipv6': True, 'ipvo': () @@ -34,7 +34,7 @@ class TestAuditConf(object): assert conf.batch is options['batch'] assert conf.colors is options['colors'] assert conf.verbose is options['verbose'] - assert conf.minlevel == options['minlevel'] + assert conf.level == options['level'] assert conf.ipv4 == options['ipv4'] assert conf.ipv6 == options['ipv6'] assert conf.ipvo == options['ipvo'] @@ -115,14 +115,14 @@ class TestAuditConf(object): conf.ipvo = (4, 4, 4, 6, 6) assert conf.ipvo == (4, 6) - def test_audit_conf_minlevel(self): + def test_audit_conf_level(self): conf = self.AuditConf() for level in ['info', 'warn', 'fail']: - conf.minlevel = level - assert conf.minlevel == level + conf.level = level + assert conf.level == level for level in ['head', 'good', 'unknown', None]: with pytest.raises(ValueError) as excinfo: - conf.minlevel = level + conf.level = level excinfo.match(r'.*invalid level.*') def test_audit_conf_cmdline(self): @@ -148,6 +148,14 @@ class TestAuditConf(object): self._test_conf(conf, host='localhost', port=2222) conf = c('-p 2222 localhost') self._test_conf(conf, host='localhost', port=2222) + conf = c('2001:4860:4860::8888') + self._test_conf(conf, host='2001:4860:4860::8888') + conf = c('[2001:4860:4860::8888]:22') + self._test_conf(conf, host='2001:4860:4860::8888') + conf = c('[2001:4860:4860::8888]:2222') + self._test_conf(conf, host='2001:4860:4860::8888', port=2222) + conf = c('-p 2222 2001:4860:4860::8888') + self._test_conf(conf, host='2001:4860:4860::8888', port=2222) with pytest.raises(SystemExit): conf = c('localhost:') with pytest.raises(SystemExit): @@ -183,10 +191,10 @@ class TestAuditConf(object): conf = c('-v localhost') self._test_conf(conf, host='localhost', verbose=True) conf = c('-l info localhost') - self._test_conf(conf, host='localhost', minlevel='info') + self._test_conf(conf, host='localhost', level='info') conf = c('-l warn localhost') - self._test_conf(conf, host='localhost', minlevel='warn') + self._test_conf(conf, host='localhost', level='warn') conf = c('-l fail localhost') - self._test_conf(conf, host='localhost', minlevel='fail') + self._test_conf(conf, host='localhost', level='fail') with pytest.raises(SystemExit): conf = c('-l something localhost') diff --git a/test/test_errors.py b/test/test_errors.py index ad35a54..abf720e 100644 --- a/test/test_errors.py +++ b/test/test_errors.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- import socket +import errno import pytest @@ -17,46 +18,99 @@ class TestErrors(object): conf.batch = True return conf + def _audit(self, spy, conf=None, sysexit=True): + if conf is None: + conf = self._conf() + spy.begin() + if sysexit: + with pytest.raises(SystemExit): + self.audit(conf) + else: + self.audit(conf) + lines = spy.flush() + return lines + + def test_connection_unresolved(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.gsock.addrinfodata['localhost#22'] = [] + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'has no DNS records' in lines[-1] + def test_connection_refused(self, output_spy, virtual_socket): vsocket = virtual_socket - vsocket.errors['connect'] = socket.error(61, 'Connection refused') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + vsocket.errors['connect'] = socket.error(errno.ECONNREFUSED, 'Connection refused') + lines = self._audit(output_spy) assert len(lines) == 1 assert 'Connection refused' in lines[-1] - def test_connection_closed_before_banner(self, output_spy, virtual_socket): + def test_connection_timeout(self, output_spy, virtual_socket): vsocket = virtual_socket - vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + vsocket.errors['connect'] = socket.timeout('timed out') + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'timed out' in lines[-1] + + def test_recv_empty(self, output_spy, virtual_socket): + vsocket = virtual_socket + lines = self._audit(output_spy) assert len(lines) == 1 assert 'did not receive banner' in lines[-1] + def test_recv_timeout(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.rdata.append(socket.timeout('timed out')) + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + assert 'timed out' in lines[-1] + + def test_recv_retry_till_timeout(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EWOULDBLOCK, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.timeout('timed out')) + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + assert 'timed out' in lines[-1] + + def test_recv_retry_till_reset(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EWOULDBLOCK, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.EAGAIN, 'Resource temporarily unavailable')) + vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer')) + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + assert 'reset by peer' in lines[-1] + + def test_connection_closed_before_banner(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer')) + lines = self._audit(output_spy) + assert len(lines) == 1 + assert 'did not receive banner' in lines[-1] + assert 'reset by peer' in lines[-1] + def test_connection_closed_after_header(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'header line 1\n') + vsocket.rdata.append(b'\n') vsocket.rdata.append(b'header line 2\n') - vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + vsocket.rdata.append(socket.error(errno.ECONNRESET, 'Connection reset by peer')) + lines = self._audit(output_spy) assert len(lines) == 3 assert 'did not receive banner' in lines[-1] + assert 'reset by peer' in lines[-1] def test_connection_closed_after_banner(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') vsocket.rdata.append(socket.error(54, 'Connection reset by peer')) - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert 'reset by peer' in lines[-1] @@ -64,10 +118,7 @@ class TestErrors(object): def test_empty_data_after_banner(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert 'empty' in lines[-1] @@ -76,10 +127,7 @@ class TestErrors(object): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') vsocket.rdata.append(b'xxx\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert 'xxx' in lines[-1] @@ -87,10 +135,7 @@ class TestErrors(object): def test_non_ascii_banner(self, output_spy, virtual_socket): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\xc3\xbc\r\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 3 assert 'error reading packet' in lines[-1] assert 'ASCII' in lines[-2] @@ -100,10 +145,7 @@ class TestErrors(object): vsocket = virtual_socket vsocket.rdata.append(b'SSH-2.0-ssh-audit-test\r\n') vsocket.rdata.append(b'\x81\xff\n') - output_spy.begin() - with pytest.raises(SystemExit): - self.audit(self._conf()) - lines = output_spy.flush() + lines = self._audit(output_spy) assert len(lines) == 2 assert 'error reading packet' in lines[-1] assert '\\x81\\xff' in lines[-1] @@ -112,12 +154,9 @@ class TestErrors(object): vsocket = virtual_socket vsocket.rdata.append(b'SSH-1.3-ssh-audit-test\r\n') vsocket.rdata.append(b'Protocol major versions differ.\n') - output_spy.begin() - with pytest.raises(SystemExit): - conf = self._conf() - conf.ssh1, conf.ssh2 = True, False - self.audit(conf) - lines = output_spy.flush() + conf = self._conf() + conf.ssh1, conf.ssh2 = True, False + lines = self._audit(output_spy, conf) assert len(lines) == 3 assert 'error reading packet' in lines[-1] assert 'major versions differ' in lines[-1] diff --git a/test/test_output.py b/test/test_output.py index 74b2c19..3ac6f06 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -41,13 +41,13 @@ class TestOutput(object): out = self.Output() # default: on assert out.batch is False - assert out.colors is True - assert out.minlevel == 'info' + assert out.use_colors is True + assert out.level == 'info' def test_output_colors(self, output_spy): out = self.Output() # test without colors - out.colors = False + out.use_colors = False output_spy.begin() out.info('info color') assert output_spy.flush() == [u'info color'] @@ -66,7 +66,7 @@ class TestOutput(object): if not out.colors_supported: return # test with colors - out.colors = True + out.use_colors = True output_spy.begin() out.info('info color') assert output_spy.flush() == [u'info color'] @@ -93,29 +93,29 @@ class TestOutput(object): def test_output_levels(self): out = self.Output() - assert out.getlevel('info') == 0 - assert out.getlevel('good') == 0 - assert out.getlevel('warn') == 1 - assert out.getlevel('fail') == 2 - assert out.getlevel('unknown') > 2 + assert out.get_level('info') == 0 + assert out.get_level('good') == 0 + assert out.get_level('warn') == 1 + assert out.get_level('fail') == 2 + assert out.get_level('unknown') > 2 - def test_output_minlevel_property(self): + def test_output_level_property(self): out = self.Output() - out.minlevel = 'info' - assert out.minlevel == 'info' - out.minlevel = 'good' - assert out.minlevel == 'info' - out.minlevel = 'warn' - assert out.minlevel == 'warn' - out.minlevel = 'fail' - assert out.minlevel == 'fail' - out.minlevel = 'invalid level' - assert out.minlevel == 'unknown' + out.level = 'info' + assert out.level == 'info' + out.level = 'good' + assert out.level == 'info' + out.level = 'warn' + assert out.level == 'warn' + out.level = 'fail' + assert out.level == 'fail' + out.level = 'invalid level' + assert out.level == 'unknown' - def test_output_minlevel(self, output_spy): + def test_output_level(self, output_spy): out = self.Output() # visible: all - out.minlevel = 'info' + out.level = 'info' output_spy.begin() out.info('info color') out.head('head color') @@ -124,7 +124,7 @@ class TestOutput(object): out.fail('fail color') assert len(output_spy.flush()) == 5 # visible: head, warn, fail - out.minlevel = 'warn' + out.level = 'warn' output_spy.begin() out.info('info color') out.head('head color') @@ -133,7 +133,7 @@ class TestOutput(object): out.fail('fail color') assert len(output_spy.flush()) == 3 # visible: head, fail - out.minlevel = 'fail' + out.level = 'fail' output_spy.begin() out.info('info color') out.head('head color') @@ -142,7 +142,7 @@ class TestOutput(object): out.fail('fail color') assert len(output_spy.flush()) == 2 # visible: head - out.minlevel = 'invalid level' + out.level = 'invalid level' output_spy.begin() out.info('info color') out.head('head color') @@ -155,7 +155,7 @@ class TestOutput(object): out = self.Output() # visible: all output_spy.begin() - out.minlevel = 'info' + out.level = 'info' out.batch = False out.info('info color') out.head('head color') @@ -165,7 +165,7 @@ class TestOutput(object): assert len(output_spy.flush()) == 5 # visible: all except head output_spy.begin() - out.minlevel = 'info' + out.level = 'info' out.batch = True out.info('info color') out.head('head color') diff --git a/test/test_resolve.py b/test/test_resolve.py new file mode 100644 index 0000000..8fcddf6 --- /dev/null +++ b/test/test_resolve.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import socket +import pytest + + +# pylint: disable=attribute-defined-outside-init,protected-access +class TestResolve(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.AuditConf = ssh_audit.AuditConf + self.audit = ssh_audit.audit + self.ssh = ssh_audit.SSH + + def _conf(self): + conf = self.AuditConf('localhost', 22) + conf.colors = False + conf.batch = True + return conf + + def test_resolve_error(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.gsock.addrinfodata['localhost#22'] = socket.gaierror(8, 'hostname nor servname provided, or not known') + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + output_spy.begin() + with pytest.raises(SystemExit): + r = list(s._resolve(conf.ipvo)) + lines = output_spy.flush() + assert len(lines) == 1 + assert 'hostname nor servname provided' in lines[-1] + + def test_resolve_hostname_without_records(self, output_spy, virtual_socket): + vsocket = virtual_socket + vsocket.gsock.addrinfodata['localhost#22'] = [] + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + output_spy.begin() + r = list(s._resolve(conf.ipvo)) + assert len(r) == 0 + + def test_resolve_ipv4(self, virtual_socket): + vsocket = virtual_socket + conf = self._conf() + conf.ipv4 = True + s = self.ssh.Socket('localhost', 22) + r = list(s._resolve(conf.ipvo)) + assert len(r) == 1 + assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) + + def test_resolve_ipv6(self, virtual_socket): + vsocket = virtual_socket + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + conf.ipv6 = True + r = list(s._resolve(conf.ipvo)) + assert len(r) == 1 + assert r[0] == (socket.AF_INET6, ('::1', 22)) + + def test_resolve_ipv46_both(self, virtual_socket): + vsocket = virtual_socket + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + r = list(s._resolve(conf.ipvo)) + assert len(r) == 2 + assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) + assert r[1] == (socket.AF_INET6, ('::1', 22)) + + def test_resolve_ipv46_order(self, virtual_socket): + vsocket = virtual_socket + s = self.ssh.Socket('localhost', 22) + conf = self._conf() + conf.ipv4 = True + conf.ipv6 = True + r = list(s._resolve(conf.ipvo)) + assert len(r) == 2 + assert r[0] == (socket.AF_INET, ('127.0.0.1', 22)) + assert r[1] == (socket.AF_INET6, ('::1', 22)) + conf = self._conf() + conf.ipv6 = True + conf.ipv4 = True + r = list(s._resolve(conf.ipvo)) + assert len(r) == 2 + assert r[0] == (socket.AF_INET6, ('::1', 22)) + assert r[1] == (socket.AF_INET, ('127.0.0.1', 22)) diff --git a/test/test_socket.py b/test/test_socket.py new file mode 100644 index 0000000..d5c27fc --- /dev/null +++ b/test/test_socket.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import socket +import pytest + + +# pylint: disable=attribute-defined-outside-init +class TestSocket(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.ssh = ssh_audit.SSH + + def test_invalid_host(self, virtual_socket): + with pytest.raises(ValueError): + s = self.ssh.Socket(None, 22) + + def test_invalid_port(self, virtual_socket): + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', 'abc') + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', -1) + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', 0) + with pytest.raises(ValueError): + s = self.ssh.Socket('localhost', 65536) + + def test_not_connected_socket(self, virtual_socket): + sock = self.ssh.Socket('localhost', 22) + banner, header, err = sock.get_banner() + assert banner is None + assert len(header) == 0 + assert err == 'not connected' + s, e = sock.recv() + assert s == -1 + assert e == 'not connected' + s, e = sock.send('nothing') + assert s == -1 + assert e == 'not connected' + s, e = sock.send_packet() + assert s == -1 + assert e == 'not connected' diff --git a/test/test_software.py b/test/test_software.py index 141ffec..4785041 100644 --- a/test/test_software.py +++ b/test/test_software.py @@ -168,17 +168,17 @@ class TestSoftware(object): assert s.display(True) == str(s) assert s.display(False) == str(s) assert repr(s) == '' - s = ps('SSH-2.0-libssh-0.7.3') + s = ps('SSH-2.0-libssh-0.7.4') assert s.vendor is None assert s.product == 'libssh' - assert s.version == '0.7.3' + assert s.version == '0.7.4' assert s.patch is None assert s.os is None - assert str(s) == 'libssh 0.7.3' + assert str(s) == 'libssh 0.7.4' assert str(s) == s.display() assert s.display(True) == str(s) assert s.display(False) == str(s) - assert repr(s) == '' + assert repr(s) == '' def test_romsshell_software(self): ps = lambda x: self.ssh.Software.parse(self.ssh.Banner.parse(x)) # noqa diff --git a/test/test_ssh1.py b/test/test_ssh1.py index 0029845..f18e4be 100644 --- a/test/test_ssh1.py +++ b/test/test_ssh1.py @@ -66,34 +66,51 @@ class TestSSH1(object): assert fp.md5 == 'MD5:9d:26:f8:39:fc:20:9d:9b:ca:cc:4a:0f:e1:93:f5:96' assert fp.sha256 == 'SHA256:vZdx3mhzbvVJmn08t/ruv8WDhJ9jfKYsCTuSzot+QIs' - def test_pkm_read(self): - pkm = self.ssh1.PublicKeyMessage.parse(self._pkm_payload()) - assert pkm is not None - assert pkm.cookie == b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' - b, e, m = self._server_key() + def _assert_pkm_keys(self, pkm, skey, hkey): + b, e, m = skey assert pkm.server_key_bits == b assert pkm.server_key_public_exponent == e assert pkm.server_key_public_modulus == m - b, e, m = self._host_key() + b, e, m = hkey assert pkm.host_key_bits == b assert pkm.host_key_public_exponent == e assert pkm.host_key_public_modulus == m - fp = self.ssh.Fingerprint(pkm.host_key_fingerprint_data) + + def _assert_pkm_fields(self, pkm, skey, hkey): + assert pkm is not None + assert pkm.cookie == b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' + self._assert_pkm_keys(pkm, skey, hkey) assert pkm.protocol_flags == 2 assert pkm.supported_ciphers_mask == 72 assert pkm.supported_ciphers == ['3des', 'blowfish'] assert pkm.supported_authentications_mask == 36 assert pkm.supported_authentications == ['rsa', 'tis'] + fp = self.ssh.Fingerprint(pkm.host_key_fingerprint_data) assert fp.md5 == 'MD5:9d:26:f8:39:fc:20:9d:9b:ca:cc:4a:0f:e1:93:f5:96' assert fp.sha256 == 'SHA256:vZdx3mhzbvVJmn08t/ruv8WDhJ9jfKYsCTuSzot+QIs' + def test_pkm_init(self): + cookie = b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' + pflags, cmask, amask = 2, 72, 36 + skey, hkey = self._server_key(), self._host_key() + pkm = self.ssh1.PublicKeyMessage(cookie, skey, hkey, pflags, cmask, amask) + self._assert_pkm_fields(pkm, skey, hkey) + for skey2 in ([], [0], [0,1], [0,1,2,3]): + with pytest.raises(ValueError): + pkm = self.ssh1.PublicKeyMessage(cookie, skey2, hkey, pflags, cmask, amask) + for hkey2 in ([], [0], [0,1], [0,1,2,3]): + with pytest.raises(ValueError): + print(hkey2) + pkm = self.ssh1.PublicKeyMessage(cookie, skey, hkey2, pflags, cmask, amask) + + def test_pkm_read(self): + pkm = self.ssh1.PublicKeyMessage.parse(self._pkm_payload()) + self._assert_pkm_fields(pkm, self._server_key(), self._host_key()) + def test_pkm_payload(self): cookie = b'\x88\x99\xaa\xbb\xcc\xdd\xee\xff' - skey = self._server_key() - hkey = self._host_key() - pflags = 2 - cmask = 72 - amask = 36 + skey, hkey = self._server_key(), self._host_key() + pflags, cmask, amask = 2, 72, 36 pkm1 = self.ssh1.PublicKeyMessage(cookie, skey, hkey, pflags, cmask, amask) pkm2 = self.ssh1.PublicKeyMessage.parse(self._pkm_payload()) assert pkm1.payload == pkm2.payload @@ -108,7 +125,7 @@ class TestSSH1(object): output_spy.begin() self.audit(self._conf()) lines = output_spy.flush() - assert len(lines) == 10 + assert len(lines) == 13 def test_ssh1_server_invalid_first_packet(self, output_spy, virtual_socket): vsocket = virtual_socket @@ -121,7 +138,7 @@ class TestSSH1(object): with pytest.raises(SystemExit): self.audit(self._conf()) lines = output_spy.flush() - assert len(lines) == 4 + assert len(lines) == 7 assert 'unknown message' in lines[-1] def test_ssh1_server_invalid_checksum(self, output_spy, virtual_socket): diff --git a/test/test_ssh_algorithm.py b/test/test_ssh_algorithm.py new file mode 100644 index 0000000..5e03529 --- /dev/null +++ b/test/test_ssh_algorithm.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import pytest + + +# pylint: disable=attribute-defined-outside-init +class TestSSHAlgorithm(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.ssh = ssh_audit.SSH + + def _tf(self, v, s=None): + return self.ssh.Algorithm.Timeframe().update(v, s) + + def test_get_ssh_version(self): + def ver(v): + return self.ssh.Algorithm.get_ssh_version(v) + + assert ver('7.5') == ('OpenSSH', '7.5', False) + assert ver('7.5C') == ('OpenSSH', '7.5', True) + assert ver('d2016.74') == ('Dropbear SSH', '2016.74', False) + assert ver('l10.7.4') == ('libssh', '0.7.4', False) + assert ver('')[1] == '' + + def test_get_since_text(self): + def gst(v): + return self.ssh.Algorithm.get_since_text(v) + + assert gst(['7.5']) == 'available since OpenSSH 7.5' + assert gst(['7.5C']) == 'available since OpenSSH 7.5 (client only)' + assert gst(['7.5,']) == 'available since OpenSSH 7.5' + assert gst(['d2016.73']) == 'available since Dropbear SSH 2016.73' + assert gst(['7.5,d2016.73']) == 'available since OpenSSH 7.5, Dropbear SSH 2016.73' + assert gst(['l10.7.4']) is None + assert gst([]) is None + + def test_timeframe_creation(self): + # pylint: disable=line-too-long,too-many-statements + def cmp_tf(v, s, r): + assert str(self._tf(v, s)) == str(r) + + cmp_tf(['6.2'], None, {'OpenSSH': ['6.2', None, '6.2', None]}) + cmp_tf(['6.2'], True, {'OpenSSH': ['6.2', None, None, None]}) + cmp_tf(['6.2'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C'], None, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C'], True, {}) + cmp_tf(['6.2C'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.1,6.2C'], None, {'OpenSSH': ['6.1', None, '6.2', None]}) + cmp_tf(['6.1,6.2C'], True, {'OpenSSH': ['6.1', None, None, None]}) + cmp_tf(['6.1,6.2C'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C,6.1'], None, {'OpenSSH': ['6.1', None, '6.2', None]}) + cmp_tf(['6.2C,6.1'], True, {'OpenSSH': ['6.1', None, None, None]}) + cmp_tf(['6.2C,6.1'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.3,6.2C'], None, {'OpenSSH': ['6.3', None, '6.2', None]}) + cmp_tf(['6.3,6.2C'], True, {'OpenSSH': ['6.3', None, None, None]}) + cmp_tf(['6.3,6.2C'], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C,6.3'], None, {'OpenSSH': ['6.3', None, '6.2', None]}) + cmp_tf(['6.2C,6.3'], True, {'OpenSSH': ['6.3', None, None, None]}) + cmp_tf(['6.2C,6.3'], False, {'OpenSSH': [None, None, '6.2', None]}) + + cmp_tf(['6.2', '6.6'], None, {'OpenSSH': ['6.2', '6.6', '6.2', '6.6']}) + cmp_tf(['6.2', '6.6'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.2C', '6.6'], None, {'OpenSSH': [None, '6.6', '6.2', '6.6']}) + cmp_tf(['6.2C', '6.6'], True, {'OpenSSH': [None, '6.6', None, None]}) + cmp_tf(['6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.1,6.2C', '6.6'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '6.6']}) + cmp_tf(['6.1,6.2C', '6.6'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.1,6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.2C,6.1', '6.6'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '6.6']}) + cmp_tf(['6.2C,6.1', '6.6'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.2C,6.1', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.3,6.2C', '6.6'], None, {'OpenSSH': ['6.3', '6.6', '6.2', '6.6']}) + cmp_tf(['6.3,6.2C', '6.6'], True, {'OpenSSH': ['6.3', '6.6', None, None]}) + cmp_tf(['6.3,6.2C', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + cmp_tf(['6.2C,6.3', '6.6'], None, {'OpenSSH': ['6.3', '6.6', '6.2', '6.6']}) + cmp_tf(['6.2C,6.3', '6.6'], True, {'OpenSSH': ['6.3', '6.6', None, None]}) + cmp_tf(['6.2C,6.3', '6.6'], False, {'OpenSSH': [None, None, '6.2', '6.6']}) + + cmp_tf(['6.2', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.2', None]}) + cmp_tf(['6.2', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C', '6.6', None], None, {'OpenSSH': [None, '6.6', '6.2', None]}) + cmp_tf(['6.2C', '6.6', None], True, {'OpenSSH': [None, '6.6', None, None]}) + cmp_tf(['6.2C', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.1,6.2C', '6.6', None], None, {'OpenSSH': ['6.1', '6.6', '6.2', None]}) + cmp_tf(['6.1,6.2C', '6.6', None], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.1,6.2C', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2C,6.1', '6.6', None], None, {'OpenSSH': ['6.1', '6.6', '6.2', None]}) + cmp_tf(['6.2C,6.1', '6.6', None], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.2C,6.1', '6.6', None], False, {'OpenSSH': [None, None, '6.2', None]}) + cmp_tf(['6.2,6.3C', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.3', None]}) + cmp_tf(['6.2,6.3C', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2,6.3C', '6.6', None], False, {'OpenSSH': [None, None, '6.3', None]}) + cmp_tf(['6.3C,6.2', '6.6', None], None, {'OpenSSH': ['6.2', '6.6', '6.3', None]}) + cmp_tf(['6.3C,6.2', '6.6', None], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.3C,6.2', '6.6', None], False, {'OpenSSH': [None, None, '6.3', None]}) + + cmp_tf(['6.2', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.2', '7.1']}) + cmp_tf(['6.2', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']}) + cmp_tf(['6.1,6.2C', '6.6', '7.1'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '7.1']}) + cmp_tf(['6.1,6.2C', '6.6', '7.1'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.1,6.2C', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']}) + cmp_tf(['6.2C,6.1', '6.6', '7.1'], None, {'OpenSSH': ['6.1', '6.6', '6.2', '7.1']}) + cmp_tf(['6.2C,6.1', '6.6', '7.1'], True, {'OpenSSH': ['6.1', '6.6', None, None]}) + cmp_tf(['6.2C,6.1', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.2', '7.1']}) + cmp_tf(['6.2,6.3C', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.3', '7.1']}) + cmp_tf(['6.2,6.3C', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.2,6.3C', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.3', '7.1']}) + cmp_tf(['6.3C,6.2', '6.6', '7.1'], None, {'OpenSSH': ['6.2', '6.6', '6.3', '7.1']}) + cmp_tf(['6.3C,6.2', '6.6', '7.1'], True, {'OpenSSH': ['6.2', '6.6', None, None]}) + cmp_tf(['6.3C,6.2', '6.6', '7.1'], False, {'OpenSSH': [None, None, '6.3', '7.1']}) + + tf1 = self._tf(['6.1,d2016.72,6.2C', '6.6,d2016.73', '7.1,d2016.74']) + tf2 = self._tf(['d2016.72,6.2C,6.1', 'd2016.73,6.6', 'd2016.74,7.1']) + tf3 = self._tf(['d2016.72,6.2C,6.1', '6.6,d2016.73', '7.1,d2016.74']) + # check without caring for output order + ov = "'OpenSSH': ['6.1', '6.6', '6.2', '7.1']" + dv = "'Dropbear SSH': ['2016.72', '2016.73', '2016.72', '2016.74']" + assert len(str(tf1)) == len(str(tf2)) == len(str(tf3)) + assert ov in str(tf1) and ov in str(tf2) and ov in str(tf3) + assert dv in str(tf1) and dv in str(tf2) and dv in str(tf3) + assert ov in repr(tf1) and ov in repr(tf2) and ov in repr(tf3) + assert dv in repr(tf1) and dv in repr(tf2) and dv in repr(tf3) + + def test_timeframe_object(self): + tf = self._tf(['6.1,6.2C', '6.6', '7.1']) + assert 'OpenSSH' in tf + assert 'Dropbear SSH' not in tf + assert 'libssh' not in tf + assert 'unknown' not in tf + assert tf['OpenSSH'] == ('6.1', '6.6', '6.2', '7.1') + assert tf['Dropbear SSH'] == (None, None, None, None) + assert tf['libssh'] == (None, None, None, None) + assert tf['unknown'] == (None, None, None, None) + assert tf.get_from('OpenSSH', True) == '6.1' + assert tf.get_till('OpenSSH', True) == '6.6' + assert tf.get_from('OpenSSH', False) == '6.2' + assert tf.get_till('OpenSSH', False) == '7.1' + + tf = self._tf(['6.1,d2016.72,6.2C', '6.6,d2016.73', '7.1,d2016.74']) + assert 'OpenSSH' in tf + assert 'Dropbear SSH' in tf + assert 'libssh' not in tf + assert 'unknown' not in tf + assert tf['OpenSSH'] == ('6.1', '6.6', '6.2', '7.1') + assert tf['Dropbear SSH'] == ('2016.72', '2016.73', '2016.72', '2016.74') + assert tf['libssh'] == (None, None, None, None) + assert tf['unknown'] == (None, None, None, None) + assert tf.get_from('OpenSSH', True) == '6.1' + assert tf.get_till('OpenSSH', True) == '6.6' + assert tf.get_from('OpenSSH', False) == '6.2' + assert tf.get_till('OpenSSH', False) == '7.1' + assert tf.get_from('Dropbear SSH', True) == '2016.72' + assert tf.get_till('Dropbear SSH', True) == '2016.73' + assert tf.get_from('Dropbear SSH', False) == '2016.72' + assert tf.get_till('Dropbear SSH', False) == '2016.74' + ov = "'OpenSSH': ['6.1', '6.6', '6.2', '7.1']" + dv = "'Dropbear SSH': ['2016.72', '2016.73', '2016.72', '2016.74']" + assert ov in str(tf) + assert dv in str(tf) + assert ov in repr(tf) + assert dv in repr(tf) diff --git a/test/test_utils.py b/test/test_utils.py new file mode 100644 index 0000000..2a83bd8 --- /dev/null +++ b/test/test_utils.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +import pytest + + +# pylint: disable=attribute-defined-outside-init +class TestUtils(object): + @pytest.fixture(autouse=True) + def init(self, ssh_audit): + self.utils = ssh_audit.Utils + self.PY3 = sys.version_info >= (3,) + + def test_to_bytes_py2(self): + if self.PY3: + return + # binary_type (native str, bytes as str) + assert self.utils.to_bytes('fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + assert self.utils.to_bytes(b'fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + # text_type (unicode) + assert self.utils.to_bytes(u'fran\xe7ais') == 'fran\xc3\xa7ais' + # other + with pytest.raises(TypeError): + self.utils.to_bytes(123) + + def test_to_bytes_py3(self): + if not self.PY3: + return + # binary_type (bytes) + assert self.utils.to_bytes(b'fran\xc3\xa7ais') == b'fran\xc3\xa7ais' + # text_type (native str as unicode, unicode) + assert self.utils.to_bytes('fran\xe7ais') == b'fran\xc3\xa7ais' + assert self.utils.to_bytes(u'fran\xe7ais') == b'fran\xc3\xa7ais' + # other + with pytest.raises(TypeError): + self.utils.to_bytes(123) + + def test_to_utext_py2(self): + if self.PY3: + return + # binary_type (native str, bytes as str) + assert self.utils.to_utext('fran\xc3\xa7ais') == u'fran\xe7ais' + assert self.utils.to_utext(b'fran\xc3\xa7ais') == u'fran\xe7ais' + # text_type (unicode) + assert self.utils.to_utext(u'fran\xe7ais') == u'fran\xe7ais' + # other + with pytest.raises(TypeError): + self.utils.to_utext(123) + + def test_to_utext_py3(self): + if not self.PY3: + return + # binary_type (bytes) + assert self.utils.to_utext(b'fran\xc3\xa7ais') == u'fran\xe7ais' + # text_type (native str as unicode, unicode) + assert self.utils.to_utext('fran\xe7ais') == 'fran\xe7ais' + assert self.utils.to_utext(u'fran\xe7ais') == u'fran\xe7ais' + # other + with pytest.raises(TypeError): + self.utils.to_utext(123) + + def test_to_ntext_py2(self): + if self.PY3: + return + # str (native str, bytes as str) + assert self.utils.to_ntext('fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + assert self.utils.to_ntext(b'fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + # text_type (unicode) + assert self.utils.to_ntext(u'fran\xe7ais') == 'fran\xc3\xa7ais' + # other + with pytest.raises(TypeError): + self.utils.to_ntext(123) + + def test_to_ntext_py3(self): + if not self.PY3: + return + # str (native str) + assert self.utils.to_ntext('fran\xc3\xa7ais') == 'fran\xc3\xa7ais' + assert self.utils.to_ntext(u'fran\xe7ais') == 'fran\xe7ais' + # binary_type (bytes) + assert self.utils.to_ntext(b'fran\xc3\xa7ais') == 'fran\xe7ais' + # other + with pytest.raises(TypeError): + self.utils.to_ntext(123) + + def test_is_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.is_ascii(u'francais') is True + assert self.utils.is_ascii(u'fran\xe7ais') is False + # str + assert self.utils.is_ascii('francais') is True + assert self.utils.is_ascii('fran\xc3\xa7ais') is False + # other + assert self.utils.is_ascii(123) is False + + def test_is_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.is_ascii('francais') is True + assert self.utils.is_ascii(u'francais') is True + assert self.utils.is_ascii('fran\xe7ais') is False + assert self.utils.is_ascii(u'fran\xe7ais') is False + # other + assert self.utils.is_ascii(123) is False + + def test_to_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.to_ascii(u'francais') == 'francais' + assert self.utils.to_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_ascii(u'fran\xe7ais', 'ignore') == 'franais' + # str + assert self.utils.to_ascii('francais') == 'francais' + assert self.utils.to_ascii('fran\xc3\xa7ais') == 'fran??ais' + assert self.utils.to_ascii('fran\xc3\xa7ais', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_ascii(123) + + def test_to_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.to_ascii('francais') == 'francais' + assert self.utils.to_ascii(u'francais') == 'francais' + assert self.utils.to_ascii('fran\xe7ais') == 'fran?ais' + assert self.utils.to_ascii('fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_ascii(u'fran\xe7ais', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_ascii(123) + + def test_is_print_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.is_print_ascii(u'francais') is True + assert self.utils.is_print_ascii(u'francais\n') is False + assert self.utils.is_print_ascii(u'fran\xe7ais') is False + assert self.utils.is_print_ascii(u'fran\xe7ais\n') is False + # str + assert self.utils.is_print_ascii('francais') is True + assert self.utils.is_print_ascii('francais\n') is False + assert self.utils.is_print_ascii('fran\xc3\xa7ais') is False + # other + assert self.utils.is_print_ascii(123) is False + + def test_is_print_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.is_print_ascii('francais') is True + assert self.utils.is_print_ascii('francais\n') is False + assert self.utils.is_print_ascii(u'francais') is True + assert self.utils.is_print_ascii(u'francais\n') is False + assert self.utils.is_print_ascii('fran\xe7ais') is False + assert self.utils.is_print_ascii(u'fran\xe7ais') is False + # other + assert self.utils.is_print_ascii(123) is False + + def test_to_print_ascii_py2(self): + if self.PY3: + return + # text_type (unicode) + assert self.utils.to_print_ascii(u'francais') == 'francais' + assert self.utils.to_print_ascii(u'francais\n') == 'francais?' + assert self.utils.to_print_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n') == 'fran?ais?' + assert self.utils.to_print_ascii(u'fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n', 'ignore') == 'franais' + # str + assert self.utils.to_print_ascii('francais') == 'francais' + assert self.utils.to_print_ascii('francais\n') == 'francais?' + assert self.utils.to_print_ascii('fran\xc3\xa7ais') == 'fran??ais' + assert self.utils.to_print_ascii('fran\xc3\xa7ais\n') == 'fran??ais?' + assert self.utils.to_print_ascii('fran\xc3\xa7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii('fran\xc3\xa7ais\n', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_print_ascii(123) + + def test_to_print_ascii_py3(self): + if not self.PY3: + return + # text_type (str) + assert self.utils.to_print_ascii('francais') == 'francais' + assert self.utils.to_print_ascii('francais\n') == 'francais?' + assert self.utils.to_print_ascii(u'francais') == 'francais' + assert self.utils.to_print_ascii(u'francais\n') == 'francais?' + assert self.utils.to_print_ascii('fran\xe7ais') == 'fran?ais' + assert self.utils.to_print_ascii('fran\xe7ais\n') == 'fran?ais?' + assert self.utils.to_print_ascii('fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii('fran\xe7ais\n', 'ignore') == 'franais' + assert self.utils.to_print_ascii(u'fran\xe7ais') == 'fran?ais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n') == 'fran?ais?' + assert self.utils.to_print_ascii(u'fran\xe7ais', 'ignore') == 'franais' + assert self.utils.to_print_ascii(u'fran\xe7ais\n', 'ignore') == 'franais' + with pytest.raises(TypeError): + self.utils.to_print_ascii(123) + + def test_ctoi(self): + assert self.utils.ctoi(123) == 123 + assert self.utils.ctoi('ABC') == 65 + + def test_parse_int(self): + assert self.utils.parse_int(123) == 123 + assert self.utils.parse_int('123') == 123 + assert self.utils.parse_int(-123) == -123 + assert self.utils.parse_int('-123') == -123 + assert self.utils.parse_int('abc') == 0 + + def test_unique_seq(self): + assert self.utils.unique_seq((1, 2, 2, 3, 3, 3)) == (1, 2, 3) + assert self.utils.unique_seq((3, 3, 3, 2, 2, 1)) == (3, 2, 1) + assert self.utils.unique_seq([1, 2, 2, 3, 3, 3]) == [1, 2, 3] + assert self.utils.unique_seq([3, 3, 3, 2, 2, 1]) == [3, 2, 1] diff --git a/test/test_version_compare.py b/test/test_version_compare.py index d3f8554..b5c4a1f 100644 --- a/test/test_version_compare.py +++ b/test/test_version_compare.py @@ -200,7 +200,7 @@ class TestVersionCompare(object): versions.append('0.5.{0}'.format(i)) for i in range(0, 6): versions.append('0.6.{0}'.format(i)) - for i in range(0, 4): + for i in range(0, 5): versions.append('0.7.{0}'.format(i)) l = len(versions) for i in range(l): diff --git a/test/tools/ci-linux.sh b/test/tools/ci-linux.sh new file mode 100755 index 0000000..0bb0253 --- /dev/null +++ b/test/tools/ci-linux.sh @@ -0,0 +1,412 @@ +#!/bin/sh + +CI_VERBOSE=1 + +ci_err_msg() { echo "[ci] error: $1" >&2; } +ci_err() { [ $1 -ne 0 ] && ci_err_msg "$2" && exit 1; } +ci_is_osx() { [ X"$(uname -s)" == X"Darwin" ]; } + +ci_get_pypy_ver() { + local _v="$1" + [ -z "$_v" ] && _v=$(python -V 2>&1) + case "$_v" in + pypy-*|pypy2-*|pypy3-*|pypy3.*) echo "$_v"; return 0 ;; + pypy|pypy2|pypy3) echo "$_v-unknown"; return 0 ;; + esac + echo "$_v" | tail -1 | grep -qi pypy + if [ $? -eq 0 ]; then + local _py_ver=$(echo "$_v" | head -1 | cut -d ' ' -sf 2) + local _pypy_ver=$(echo "$_v" | tail -1 | cut -d ' ' -sf 2) + [ -z "${_py_ver} " ] && _py_ver=2 + [ -z "${_pypy_ver}" ] && _pypy_ver="unknown" + case "${_py_ver}" in + 2*) echo "pypy-${_pypy_ver}" ;; + 3.3*) echo "pypy3.3-${_pypy_ver}" ;; + 3.5*) echo "pypy3.5-${_pypy_ver}" ;; + *) echo "pypy3-${_pypy_ver}" ;; + esac + return 0 + else + return 1 + fi +} + +ci_get_py_ver() { + local _v + case "$1" in + py26) _v=2.6.9 ;; + py27) _v=2.7.13 ;; + py33) _v=3.3.6 ;; + py34) _v=3.4.6 ;; + py35) _v=3.5.3 ;; + py36) _v=3.6.1 ;; + py37) _v=3.7-dev ;; + pypy) ci_is_osx && _v=pypy2-5.7.0 || _v=pypy-portable-5.7.0 ;; + pypy3) ci_is_osx && _v=pypy3.3-5.5-alpha || _v=pypy3-portable-5.7.0 ;; + *) + [ -z "$1" ] && set -- "$(python -V 2>&1)" + _v=$(ci_get_pypy_ver "$1") + [ -z "$_v" ] && _v=$(echo "$_v" | head -1 | cut -d ' ' -sf 2) + ;; + esac + echo "${_v}" + return 0 +} + +ci_get_py_env() { + [ -z "$1" ] && set -- "$(python -V 2>&1)" + case "$(ci_get_pypy_ver "$1")" in + pypy|pypy2|pypy-*|pypy2-*) echo "pypy" ;; + pypy3|pypy3*) echo "pypy3" ;; + *) + local _v=$(echo "$1" | head -1 | sed -e 's/[^0-9]//g' | cut -c1-2) + echo "py${_v}" + esac + return 0 +} + +ci_pyenv_setup() { + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] install pyenv" + rm -rf ~/.pyenv + git clone --depth 1 https://github.com/yyuu/pyenv.git ~/.pyenv + PYENV_ROOT=$HOME/.pyenv + PATH="$HOME/.pyenv/bin:$PATH" + eval "$(pyenv init -)" + ci_err $? "failed to init pyenv" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv init: $(pyenv -v 2>&1)" + return 0 +} + +ci_pyenv_install() { + CI_PYENV_CACHE=~/.pyenv.cache + type pyenv > /dev/null 2>&1 + ci_err $? "pyenv not found" + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + local _nocache + case "${_py_env}" in + py37) _nocache=1 ;; + esac + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv install: ${_py_env}/${_py_ver}" + [ -z "${PYENV_ROOT}" ] && PYENV_ROOT="$HOME/.pyenv" + local _py_ver_dir="${PYENV_ROOT}/versions/${_py_ver}" + local _py_ver_cached_dir="${CI_PYENV_CACHE}/${_py_ver}" + if [ -z "${_nocache}" ]; then + if [ ! -d "${_py_ver_dir}" ]; then + if [ -d "${_py_ver_cached_dir}" ]; then + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv reuse ${_py_ver}" + ln -s "${_py_ver_cached_dir}" "${_py_ver_dir}" + fi + fi + fi + if [ ! -d "${_py_ver_dir}" ]; then + pyenv install -s "${_py_ver}" + ci_err $? "pyenv failed to install ${_py_ver}" + if [ -z "${_nocache}" ]; then + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv cache ${_py_ver}" + rm -rf -- "${_py_ver_cached_dir}" + mkdir -p -- "${CI_PYENV_CACHE}" + mv "${_py_ver_dir}" "${_py_ver_cached_dir}" + ln -s "${_py_ver_cached_dir}" "${_py_ver_dir}" + fi + fi + pyenv rehash + return 0 +} + +ci_pyenv_use() { + type pyenv > /dev/null 2>&1 + ci_err $? "pyenv not found" + local _py_ver=$(ci_get_py_ver "$1") + pyenv shell "${_py_ver}" + ci_err $? "pyenv could not use ${_py_ver}" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] pyenv using python: $(python -V 2>&1)" + return 0 +} + +ci_pip_setup() { + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] install pip/venv for ${_py_env}/${_py_ver}" + PIPOPT=$(python -c 'import sys; print("" if hasattr(sys, "real_prefix") else "--user")') + if [ -z "${_py_env##py2*}" ]; then + curl -O https://bootstrap.pypa.io/get-pip.py + python get-pip.py ${PIPOPT} + ci_err $? "failed to install pip" + fi + if [ X"${_py_env}" == X"py26" ]; then + python -c 'import pip; pip.main();' install ${PIPOPT} -U pip virtualenv + else + python -m pip install ${PIPOPT} -U pip virtualenv + fi + ci_err $? "failed to upgrade pip/venv" || return 0 +} + +ci_venv_setup() { + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] create venv for ${_py_env}/${_py_ver}" + local VENV_DIR=~/.venv/${_py_ver} + mkdir -p -- ~/.venv + rm -rf -- "${VENV_DIR}" + if [ X"${_py_env}" == X"py26" ]; then + python -c 'import virtualenv; virtualenv.main();' "${VENV_DIR}" + else + python -m virtualenv "${VENV_DIR}" + fi + ci_err $? "failed to create venv" || return 0 +} + +ci_venv_use() { + local _py_ver=$(ci_get_py_ver "$1") + local _py_env=$(ci_get_py_env "${_py_ver}") + local VENV_DIR=~/.venv/${_py_ver} + . "${VENV_DIR}/bin/activate" + ci_err $? "could not actiavte virtualenv" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] venv using python: $(python -V 2>&1)" + return 0 +} + +ci_get_filedir() { + local _sdir=$(cd -- "$(dirname "$0")" && pwd) + local _pdir=$(pwd) + if [ -z "${_pdir##${_sdir}*}" ]; then + _sdir="${_pdir}" + fi + local _first=1 + while [ X"${_sdir}" != X"/" ]; do + if [ ${_first} -eq 1 ]; then + _first=0 + local _f=$(find "${_sdir}" -name "$1" | head -1) + if [ -n "${_f}" ]; then + echo $(dirname -- "${_f}") + return 0 + fi + else + _f=$(find "${_sdir}" -mindepth 1 -maxdepth 1 -name "$1" | head -1) + fi + [ -n "${_f}" ] && echo "${_sdir}" && return 0 + _sdir=$(cd -- "${_sdir}/.." && pwd) + done + return 1 +} + +ci_sq_ensure_java() { + type java >/dev/null 2>&1 + if [ $? -ne 0 ]; then + ci_err_msg "java not found" + return 1 + fi + local _java_ver=$(java -version 2>&1 | head -1 | sed -e 's/[^0-9\._]//g') + if [ -z "${_java_ver##1.8*}" ]; then + return 0 + fi + ci_err_msg "unsupported java version: ${_java_ver}" + return 1 +} + +ci_sq_ensure_scanner() { + local _cli_version="3.0.0.702" + local _cli_basedir="$HOME/.bin" + local _cli_postfix="" + case "$(uname -s)" in + Linux) + [ X"$(uname -m)" = X"x86_64" ] && _cli_postfix="-linux" + [ X"$(uname -m)" = X"amd64" ] && _cli_postfix="-linux" + ;; + Darwin) _cli_postfix="-macosx" ;; + esac + if [ X"${_cli_postfix}" = X"" ]; then + ci_sq_ensure_java || return 1 + fi + if [ X"${SONAR_SCANNER_PATH}" != X"" ]; then + if [ -e "${SONAR_SCANNER_PATH}" ]; then + return 0 + fi + fi + local _cli_fname="sonar-scanner-cli-${_cli_version}${_cli_postfix}" + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] ensure scanner ${_cli_fname}" + local _cli_dname="sonar-scanner-${_cli_version}${_cli_postfix}" + local _cli_archive="${_cli_basedir}/${_cli_fname}.zip" + local _cli_dir="${_cli_basedir}/${_cli_dname}" + local _cli_url="https://sonarsource.bintray.com/Distribution/sonar-scanner-cli/${_cli_fname}.zip" + if [ ! -e "${_cli_archive}" ]; then + mkdir -p -- "${_cli_basedir}" > /dev/null 2>&1 + if [ $? -ne 0 ]; then + ci_err_msg "could not create ${_cli_basedir}" + return 1 + fi + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] downloading ${_cli_fname}" + curl -kL -o "${_cli_archive}" "${_cli_url}" + [ $? -ne 0 ] && ci_err_msg "download failed" && return 1 + [ ! -e "${_cli_archive}" ] && ci_err_msg "download verify" && return 1 + fi + if [ ! -d "${_cli_dir}" ]; then + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] extracting ${_cli_fname}" + unzip -od "${_cli_basedir}" "${_cli_archive}" + [ $? -ne 0 ] && ci_err_msg "extract failed" && return 1 + [ ! -d "${_cli_dir}" ] && ci_err_msg "extract verify" && return 1 + fi + if [ ! -e "${_cli_dir}/bin/sonar-scanner" ]; then + ci_err_msg "sonar-scanner binary not found." + return 1 + fi + SONAR_SCANNER_PATH="${_cli_dir}/bin/sonar-scanner" + return 0 +} + +ci_sq_run() { + if [ X"${SONAR_SCANNER_PATH}" = X"" ]; then + ci_err_msg "environment variable SONAR_SCANNER_PATH not set" + return 1 + fi + if [ X"${SONAR_HOST_URL}" = X"" ]; then + ci_err_msg "environment variable SONAR_HOST_URL not set" + return 1 + fi + if [ X"${SONAR_AUTH_TOKEN}" = X"" ]; then + ci_err_msg "environment variable SONAR_AUTH_TOKEN not set" + return 1 + fi + local _pdir=$(ci_get_filedir "ssh-audit.py") + if [ -z "${_pdir}" ]; then + ci_err_msg "failed to find project directory" + return 1 + fi + local _odir=$(pwd) + cd -- "${_pdir}" + local _branch=$(git name-rev --name-only HEAD | cut -d '~' -f 1) + case "${_branch}" in + master) ;; + develop) ;; + *) ci_err_msg "unknown branch: ${_branch}"; return 1 ;; + esac + local _junit=$(cd -- "${_pdir}" && ls -1 reports/junit.*.xml | sort -r | head -1) + if [ X"${_junit}" = X"" ]; then + ci_err_msg "no junit.xml found" + return 1 + fi + local _project_ver=$(grep VERSION ssh-audit.py | head -1 | cut -d "'" -f 2) + if [ -z "${_project_ver}" ]; then + ci_err_msg "failed to get project version" + return 1 + fi + if [ -z "${_project_ver##*dev}" ]; then + local _git_commit=$(git rev-parse --short=8 HEAD) + _project_ver="${_project_ver}.${_git_commit}" + fi + [ ${CI_VERBOSE} -gt 0 ] && echo "[ci] run sonar-scanner for ${_project_ver}" + "${SONAR_SCANNER_PATH}" -X \ + -Dsonar.projectKey=arthepsy-github:ssh-audit \ + -Dsonar.sources=ssh-audit.py \ + -Dsonar.tests=test \ + -Dsonar.test.inclusions=test/*.py \ + -Dsonar.host.url="${SONAR_HOST_URL}" \ + -Dsonar.projectName=ssh-audit \ + -Dsonar.projectVersion="${_project_ver}" \ + -Dsonar.branch="${_branch}" \ + -Dsonar.python.coverage.overallReportPath=reports/coverage.xml \ + -Dsonar.python.xunit.reportPath="${_junit}" \ + -Dsonar.organization=arthepsy-github \ + -Dsonar.login="${SONAR_AUTH_TOKEN}" + cd -- "${_odir}" + return 0 +} + +ci_run_wrapped() { + local _versions=$(echo "${PY_VER}" | sed -e 's/,/ /g') + [ -z "${_versions}" ] && eval "$1" + for _i in ${_versions}; do + local _v=$(echo "$_i" | cut -d '/' -f 1) + local _o=$(echo "$_i" | cut -d '/' -sf 2) + [ -z "${_o}" ] && _o="${PY_ORIGIN}" + eval "$1" "${_v}" "${_o}" || return 1 + done + return 0 +} + +ci_step_before_install_wrapped() { + local _py_ver="$1" + local _py_ori="$2" + case "${_py_ori}" in + pyenv) + if [ "${CI_PYENV_SETUP}" -eq 0 ]; then + ci_pyenv_setup + CI_PYENV_SETUP=1 + fi + ci_pyenv_install "${_py_ver}" || return 1 + ci_pyenv_use "${_py_ver}" || return 1 + ;; + esac + ci_pip_setup "${_py_ver}" || return 1 + ci_venv_setup "${_py_ver}" || return 1 + return 0 +} + +ci_step_before_install() { + if ci_is_osx; then + [ ${CI_VERBOSE} -gt 0 ] && sw_vers + brew update || brew update + brew install autoconf pkg-config openssl readline xz + brew upgrade autoconf pkg-config openssl readline xz + PY_ORIGIN=pyenv + fi + CI_PYENV_SETUP=0 + ci_run_wrapped "ci_step_before_install_wrapped" || return 1 + if [ "${CI_PYENV_SETUP}" -eq 1 ]; then + pyenv shell --unset + [ ${CI_VERBOSE} -gt 0 ] && pyenv versions + fi + return 0 +} + +ci_step_install_wrapped() { + local _py_ver="$1" + ci_venv_use "${_py_ver}" + pip install -U tox coveralls codecov + ci_err $? "failed to install dependencies" || return 0 +} + +ci_step_script_wrapped() { + local _py_ver="$1" + local _py_ori="$2" + local _py_env=$(ci_get_py_env "${_py_ver}") + ci_venv_use "${_py_ver}" || return 1 + if [ -z "${_py_env##*py3*}" ]; then + if [ -z "${_py_env##*pypy3*}" ]; then + # NOTE: workaround for travis environment + _pydir=$(dirname $(which python)) + ln -s -- "${_pydir}/python" "${_pydir}/pypy3" + # NOTE: do not lint, as it hangs when flake8 is run + # NOTE: do not type, as it can't install dependencies + TOXENV=${_py_env}-test + else + TOXENV=${_py_env}-test,${_py_env}-type,${_py_env}-lint + fi + else + # NOTE: do not type, as it isn't supported on py2x + TOXENV=${_py_env}-test,${_py_env}-lint + fi + tox -e $TOXENV,cov + ci_err $? "tox failed" || return 0 +} + +ci_step_success_wrapped() { + local _py_ver="$1" + local _py_ori="$2" + if [ X"${SQ}" = X"1" ]; then + ci_sq_ensure_scanner && ci_sq_run + fi + ci_venv_use "${_py_ver}" || return 1 + coveralls + codecov +} + +ci_step_failure() { + cat .tox/log/* + cat .tox/*/log/* +} + +ci_step_install() { ci_run_wrapped "ci_step_install_wrapped"; } +ci_step_script() { ci_run_wrapped "ci_step_script_wrapped"; } +ci_step_success() { ci_run_wrapped "ci_step_success_wrapped"; } diff --git a/test/tools/ci-win.cmd b/test/tools/ci-win.cmd new file mode 100644 index 0000000..103036c --- /dev/null +++ b/test/tools/ci-win.cmd @@ -0,0 +1,131 @@ +@ECHO OFF + +IF "%PYTHON%" == "" ( + ECHO PYTHON environment variable not set + EXIT 1 +) +SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" +FOR /F %%i IN ('python -c "import platform; print(platform.python_version());"') DO ( + SET PYTHON_VERSION=%%i +) +SET PYTHON_VERSION_MAJOR=%PYTHON_VERSION:~0,1% +IF "%PYTHON_VERSION:~3,1%" == "." ( + SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,1% +) ELSE ( + SET PYTHON_VERSION_MINOR=%PYTHON_VERSION:~2,2% +) +FOR /F %%i IN ('python -c "import struct; print(struct.calcsize(\"P\")*8)"') DO ( + SET PYTHON_ARCH=%%i +) +CALL :devenv + +IF /I "%1"=="" ( + SET target=test +) ELSE ( + SET target=%1 +) + +echo [CI] TARGET=%target% +GOTO %target% + +:devenv +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET VS2015_ROOT=C:\Program Files (x86)\Microsoft Visual Studio 14.0 +IF %PYTHON_VERSION_MAJOR% == 2 ( + SET WINDOWS_SDK_VERSION="v7.0" +) ELSE IF %PYTHON_VERSION_MAJOR% == 3 ( + IF %PYTHON_VERSION_MAJOR% LEQ 4 ( + SET WINDOWS_SDK_VERSION="v7.1" + ) ELSE ( + SET WINDOWS_SDK_VERSION="2015" + ) +) ELSE ( + ECHO Unsupported Python version: "%PYTHON_VERSION%" + EXIT 1 +) +SETLOCAL ENABLEDELAYEDEXPANSION +IF %PYTHON_ARCH% == 32 (SET PYTHON_ARCHX=x86) ELSE (SET PYTHON_ARCHX=x64) +IF %WINDOWS_SDK_VERSION% == "2015" ( + "%VS2015_ROOT%\VC\vcvarsall.bat" %PYTHON_ARCHX% +) ELSE ( + SET DISTUTILS_USE_SDK=1 + SET MSSdk=1 + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /%PYTHON_ARCHX% /release +) +GOTO :eof + +:install +pip install --user --upgrade pip virtualenv +SET VENV_DIR=.venv\%PYTHON_VERSION% +rmdir /s /q %VENV_DIR% > nul 2>nul +mkdir .venv > nul 2>nul +IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" ( + python -c "import virtualenv; virtualenv.main();" %VENV_DIR% +) ELSE ( + python -m virtualenv %VENV_DIR% +) +CALL %VENV_DIR%\Scripts\activate +python -V +pip install tox +deactivate +GOTO :eof + +:install_deps +SET LXML_FILE= +SET LXML_URL= +IF %PYTHON_VERSION_MAJOR% == 3 ( + IF %PYTHON_VERSION_MINOR% == 3 ( + IF %PYTHON_ARCH% == 32 ( + SET LXML_FILE=lxml-3.7.3.win32-py3.3.exe + SET LXML_URL=https://pypi.python.org/packages/66/fd/b82a54e7a15e91184efeef4b659379d0581a73cf78239d70feb0f0877841/lxml-3.7.3.win32-py3.3.exe + ) ELSE ( + SET LXML_FILE=lxml-3.7.3.win-amd64-py3.3.exe + SET LXML_URL=https://pypi.python.org/packages/dc/bc/4742b84793fa1fd991b5d2c6f2e5d32695659d6cfedf5c66aef9274a8723/lxml-3.7.3.win-amd64-py3.3.exe + ) + ) ELSE IF %PYTHON_VERSION_MINOR% == 4 ( + IF %PYTHON_ARCH% == 32 ( + SET LXML_FILE=lxml-3.7.3.win32-py3.4.exe + SET LXML_URL=https://pypi.python.org/packages/88/33/265459d68d465ddc707621e6471989f5c2cb0d43f230f516800ffd629af7/lxml-3.7.3.win32-py3.4.exe + ) ELSE ( + SET LXML_FILE=lxml-3.7.3.win-amd64-py3.4.exe + SET LXML_URL=https://pypi.python.org/packages/2d/65/e47db7f36a69a1b59b4f661e42d699d6c43e663b8fd91035e6f7681d017e/lxml-3.7.3.win-amd64-py3.4.exe + ) + ) +) +IF NOT "%LXML_FILE%" == "" ( + CALL :download %LXML_URL% .downloads\%LXML_FILE% + easy_install --user .downloads\%LXML_FILE% +) +GOTO :eof + +:test + SET VENV_DIR=.venv\%PYTHON_VERSION% + CALL %VENV_DIR%\Scripts\activate + IF "%TOXENV%" == "" ( + SET TOXENV=py%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR% + ) + IF "%PYTHON_VERSION_MAJOR%%PYTHON_VERSION_MINOR%" == "26" ( + SET TOX=python -c "from tox import cmdline; cmdline()" + ) ELSE ( + SET TOX=python -m tox + ) + IF %PYTHON_VERSION_MAJOR% == 3 ( + IF %PYTHON_VERSION_MINOR% LEQ 4 ( + :: Python 3.3 and 3.4 does not support typed-ast (mypy dependency) + %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1 + ) ELSE ( + %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-type,%TOXENV%-lint,cov || EXIT 1 + ) + ) ELSE ( + %TOX% --sitepackages -e %TOXENV%-test,%TOXENV%-lint,cov || EXIT 1 + ) +GOTO :eof + +:download +IF NOT EXIST %2 ( + IF NOT EXIST .downloads\ mkdir .downloads + powershell -command "(new-object net.webclient).DownloadFile('%1', '%2')" || EXIT 1 + +) +GOTO :eof diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7f61a11 --- /dev/null +++ b/tox.ini @@ -0,0 +1,158 @@ +[tox] +envlist = + py26-{test,vulture} + py{27,py,py3}-{test,pylint,flake8,vulture} + py{33,34,35,36,37}-{test,mypy,pylint,flake8,vulture} + cov +skipsdist = true +skip_missing_interpreters = true + +[testenv] +deps = + test: pytest==3.0.7 + test,cov: {[testenv:cov]deps} + test,py{33,34,35,36,37}-{type,mypy}: colorama==0.3.7 + py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]deps} + py{27,py,py3,33,34,35,36,37}-{lint,pylint},lint: {[testenv:pylint]deps} + py{27,py,py3,33,34,35,36,37}-{lint,flake8},lint: {[testenv:flake8]deps} + py{27,py,py3,33,34,35,36,37}-{lint,vulture},lint: {[testenv:vulture]deps} +setenv = + SSHAUDIT = {toxinidir}/ssh-audit.py + test: COVERAGE_FILE = {toxinidir}/.coverage.{envname} + type,mypy: MYPYPATH = {toxinidir}/test/stubs + type,mypy: MYPYHTML = {toxinidir}/reports/html/mypy +commands = + test: coverage run --source ssh-audit -m -- \ + test: pytest -v --junitxml={toxinidir}/reports/junit.{envname}.xml {posargs:test} + test: coverage report --show-missing + test: coverage html -d {toxinidir}/reports/html/coverage.{envname} + py{33,34,35,36,37}-{type,mypy}: {[testenv:mypy]commands} + py{27,py,py3,33,34,35,36,37}-{lint,pylint},lint: {[testenv:pylint]commands} + py{27,py,py3,33,34,35,36,37}-{lint,flake8},lint: {[testenv:flake8]commands} + py{27,py,py3,33,34,35,36,37}-{lint,vulture},lint: {[testenv:vulture]commands} +ignore_outcome = + type: true + lint: true + +[testenv:cov] +deps = + coverage==4.3.4 +setenv = + COVERAGE_FILE = {toxinidir}/.coverage +commands = + coverage erase + coverage combine + coverage report --show-missing + coverage xml -i -o {toxinidir}/reports/coverage.xml + coverage html -d {toxinidir}/reports/html/coverage + +[testenv:mypy] +deps = + colorama==0.3.7 + lxml==3.7.3 + mypy==0.501 +commands = + mypy \ + --show-error-context \ + --config-file {toxinidir}/tox.ini \ + --html-report {env:MYPYHTML}.py3.{envname} \ + {posargs:{env:SSHAUDIT}} + mypy \ + -2 \ + --no-warn-incomplete-stub \ + --show-error-context \ + --config-file {toxinidir}/tox.ini \ + --html-report {env:MYPYHTML}.py2.{envname} \ + {posargs:{env:SSHAUDIT}} + +[testenv:pylint] +deps = + mccabe + pylint +commands = + pylint \ + --rcfile tox.ini \ + --load-plugins=pylint.extensions.bad_builtin \ + --load-plugins=pylint.extensions.check_elif \ + --load-plugins=pylint.extensions.mccabe \ + {posargs:{env:SSHAUDIT}} + +[testenv:flake8] +deps = + flake8 +commands = + flake8 {posargs:{env:SSHAUDIT}} + +[testenv:vulture] +deps = + vulture +commands = + python -c "import sys; from subprocess import Popen, PIPE; \ + a = ['vulture'] + r'{posargs:{env:SSHAUDIT}}'.split(' '); \ + o = Popen(a, shell=False, stdout=PIPE).communicate()[0]; \ + l = [x for x in o.split(b'\n') if x and b'Unused import' not in x]; \ + print(b'\n'.join(l).decode('utf-8')); \ + sys.exit(1 if len(l) > 0 else 0)" + + +[mypy] +ignore_missing_imports = False +follow_imports = error +disallow_untyped_calls = True +disallow_untyped_defs = True +check_untyped_defs = True +disallow_subclassing_any = True +warn_incomplete_stub = True +warn_redundant_casts = True +warn_return_any = True +warn_unused_ignores = True +strict_optional = True +strict_boolean = True + +[pylint] +reports = no +#output-format = colorized +indent-string = \t +disable = + locally-disabled, + bad-continuation, + multiple-imports, + invalid-name, + trailing-whitespace, + missing-docstring +max-complexity = 15 +max-args = 8 +max-locals = 20 +max-returns = 6 +max-branches = 15 +max-statements = 60 +max-parents = 7 +max-attributes = 8 +min-public-methods = 1 +max-public-methods = 20 +max-bool-expr = 5 +max-nested-blocks = 6 +max-line-length = 80 +ignore-long-lines = ^\s*(#\s+type:\s+.*|[A-Z0-9_]+\s+=\s+.*|('.*':\s+)?\[.*\],?|assert\s+.*)$ +max-module-lines = 2500 + +[flake8] +ignore = + # indentation contains tabs + W191, + # blank line contains whitespace + W293, + # indentation contains mixed spaces and tabs + E101, + # multiple spaces before operator + E221, + # multiple spaces after operator + E241, + # multiple imports on one line + E401, + # line too long + E501, + # module imported but unused + F401, + # undefined name + F821