Skip to content

Commit db16f71

Browse files
committed
Merge branch 'known-hosts'
2 parents 28f0162 + 66bb4f3 commit db16f71

File tree

8 files changed

+210
-25
lines changed

8 files changed

+210
-25
lines changed

src/clj_libssh2/error.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
(def timeouts (atom {:agent 10000
77
:auth 10000
8+
:known-hosts 10000
89
:session 10000}))
910

1011
(def error-messages

src/clj_libssh2/known_hosts.clj

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
(ns clj-libssh2.known-hosts
2+
(:require [clojure.java.io :refer [file]]
3+
[clj-libssh2.error :refer [handle-errors with-timeout]]
4+
[clj-libssh2.libssh2 :as libssh2]
5+
[clj-libssh2.libssh2.knownhost :as libssh2-knownhost]
6+
[clj-libssh2.libssh2.session :as libssh2-session])
7+
(:import [com.sun.jna.ptr IntByReference PointerByReference]))
8+
9+
(defn- checkp-result
10+
"Re-interpret the result of libssh2_knownhost_checkp to either succeed or
11+
cause handle-errors to throw, as appropriate."
12+
[fail-on-mismatch fail-on-missing result]
13+
(condp = (.longValue result)
14+
libssh2/KNOWNHOST_CHECK_MATCH 0
15+
libssh2/KNOWNHOST_CHECK_MISMATCH (if fail-on-mismatch
16+
libssh2/ERROR_HOSTKEY_SIGN
17+
0)
18+
libssh2/KNOWNHOST_CHECK_NOTFOUND (if fail-on-missing
19+
libssh2/ERROR_HOSTKEY_SIGN
20+
0)
21+
libssh2/KNOWNHOST_CHECK_FAILURE libssh2/ERROR_HOSTKEY_SIGN
22+
(throw (Exception. (format "Unknown return code from libssh2-knownhost/checkp: %d" result)))))
23+
24+
(defn- host-fingerprint
25+
"Get the remote host's fingerprint."
26+
[session]
27+
(when (:session session)
28+
(let [len (IntByReference.)
29+
typ (IntByReference.)
30+
hostkey_ptr (libssh2-session/hostkey (:session session) len typ)]
31+
(.getByteArray hostkey_ptr 0 (.getValue len)))))
32+
33+
(defn- check-fingerprint
34+
"Call libssh2_knownhost_checkp."
35+
[session known-hosts host port fingerprint fail-on-missing fail-on-mismatch]
36+
(handle-errors session
37+
(with-timeout :known-hosts
38+
(checkp-result fail-on-mismatch fail-on-missing
39+
(libssh2-knownhost/checkp known-hosts
40+
host
41+
port
42+
fingerprint
43+
(count fingerprint)
44+
(bit-or libssh2/KNOWNHOST_TYPE_PLAIN
45+
libssh2/KNOWNHOST_KEYENC_RAW)
46+
(PointerByReference.))))))
47+
48+
(defn- load-known-hosts
49+
"Load a known hosts file into the known hosts object."
50+
[session known-hosts known-hosts-file]
51+
(when (.exists (file known-hosts-file))
52+
(handle-errors session
53+
(with-timeout :known-hosts
54+
(libssh2-knownhost/readfile known-hosts
55+
known-hosts-file
56+
libssh2/KNOWNHOST_FILE_OPENSSH)))))
57+
58+
(defn check
59+
"Given a session that has already completed a handshake with a remote host,
60+
check the fingerprint of the remote host against the knonw hosts file."
61+
[session]
62+
(let [known-hosts (libssh2-knownhost/init (:session session))
63+
session-options (:options session)
64+
file (or (:known-hosts-file session-options)
65+
(str (System/getProperty "user.home") "/.ssh/known_hosts"))
66+
fail-on-mismatch (-> session-options :fail-unless-known-hosts-matches)
67+
fail-on-missing (-> session-options :fail-if-not-in-known-hosts)]
68+
(when (nil? known-hosts)
69+
(throw (Exception. "Failed to initialize known hosts store.")))
70+
(try
71+
(load-known-hosts session known-hosts file)
72+
(check-fingerprint session
73+
known-hosts
74+
(:host session)
75+
(:port session)
76+
(host-fingerprint session)
77+
fail-on-missing
78+
fail-on-mismatch)
79+
(finally (libssh2-knownhost/free known-hosts)))))

src/clj_libssh2/libssh2/session.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
; const char *libssh2_session_hostkey(LIBSSH2_SESSION *session,
5252
; size_t *len,
5353
; int *type);
54-
(def hostkey (jna/to-fn String ssh2/libssh2_session_hostkey))
54+
(def hostkey (jna/to-fn Pointer ssh2/libssh2_session_hostkey))
5555

5656
; LIBSSH2_SESSION * libssh2_session_init_ex(LIBSSH2_ALLOC_FUNC((*my_alloc)),
5757
; LIBSSH2_FREE_FUNC((*my_free)),

src/clj_libssh2/session.clj

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
[clj-libssh2.libssh2.session :as libssh2-session]
44
[clj-libssh2.authentication :refer [authenticate]]
55
[clj-libssh2.error :refer [handle-errors with-timeout]]
6+
[clj-libssh2.known-hosts :as known-hosts]
67
[clj-libssh2.socket :as socket]))
78

89
(def sessions (atom {}))
910

10-
(defrecord Session [id session socket])
11+
; The default options for a session. These are not only the defaults, but an
12+
; exhaustive list of the legal options.
13+
(def default-opts
14+
{:fail-if-not-in-known-hosts false
15+
:fail-unless-known-hosts-matches true
16+
:known-hosts-file nil})
17+
18+
(defrecord Session [id session socket host port options])
1119

1220
(defn- create-session
1321
"Make a native libssh2 session object."
@@ -17,6 +25,11 @@
1725
(throw (Exception. "Failed to create a libssh2 session.")))
1826
session))
1927

28+
(defn- create-session-options
29+
[opts]
30+
{:pre [(every? (set (keys default-opts)) (keys opts))]}
31+
(merge default-opts opts))
32+
2033
(defn- destroy-session
2134
"Free a libssh2 session object from a Session."
2235
([^Session session]
@@ -38,9 +51,14 @@
3851
(libssh2-session/handshake session socket))))
3952

4053
(defn- session-id
41-
"Generate the session ID that will be used to pool"
42-
[host port credentials]
43-
(format "%s@%s:%d" (:username credentials) host port))
54+
"Generate the session ID that will be used to pool sessions."
55+
[host port credentials opts]
56+
(format "%s@%s:%d-%d-%d"
57+
(:username credentials)
58+
host
59+
port
60+
(.hashCode credentials)
61+
(.hashCode opts)))
4462

4563
(defn close
4664
"Disconnect an SSH session and discard the session."
@@ -54,22 +72,29 @@
5472

5573
(defn open
5674
"Connect to an SSH server and start a session."
57-
[host port credentials]
58-
(when (empty? @sessions)
59-
(handle-errors nil (libssh2/init 0)))
60-
(let [id (session-id host port credentials)]
61-
(if (contains? @sessions id)
62-
(get @sessions id)
63-
(let [session (Session. (session-id host port credentials)
64-
(create-session)
65-
(socket/connect host port))]
66-
(when (> 0 (:socket session))
67-
(destroy-session session "Shutting down due to bad socket." true))
68-
(try
69-
(handshake session)
70-
(authenticate session credentials)
71-
(swap! sessions assoc (:id session) session)
72-
(get @sessions (:id session))
73-
(catch Exception e
74-
(close session)
75-
(throw e)))))))
75+
([host port credentials]
76+
(open host port credentials {}))
77+
([host port credentials opts]
78+
(when (empty? @sessions)
79+
(handle-errors nil (libssh2/init 0)))
80+
(let [session-options (create-session-options opts)
81+
id (session-id host port credentials session-options)]
82+
(if (contains? @sessions id)
83+
(get @sessions id)
84+
(let [session (Session. id
85+
(create-session)
86+
(socket/connect host port)
87+
host
88+
port
89+
(create-session-options opts))]
90+
(when (> 0 (:socket session))
91+
(destroy-session session "Shutting down due to bad socket." true))
92+
(try
93+
(handshake session)
94+
(known-hosts/check session)
95+
(authenticate session credentials)
96+
(swap! sessions assoc (:id session) session)
97+
(get @sessions (:id session))
98+
(catch Exception e
99+
(close session)
100+
(throw e))))))))

test/clj_libssh2/test_known_hosts.clj

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
(ns clj-libssh2.test-known-hosts
2+
(:require [clojure.test :refer :all]
3+
[clj-libssh2.session :as session]
4+
[clj-libssh2.test-utils :as test]))
5+
6+
(test/fixtures)
7+
8+
(defn- session-with-options
9+
[options]
10+
(session/open test/ssh-host
11+
test/ssh-port
12+
{:username (test/ssh-user)}
13+
options))
14+
15+
(deftest by-default-we-don't-fail-if-the-host-is-unknown
16+
(is (= 0 (count @session/sessions)))
17+
(let [file (test/known-hosts-file :missing)
18+
session (session-with-options {:known-hosts-file file})]
19+
(is (= 1 (count @session/sessions)))
20+
(session/close session))
21+
(is (= 0 (count @session/sessions))))
22+
23+
(deftest by-default-we-fail-if-the-host-is-different
24+
(is (= 0 (count @session/sessions)))
25+
(is (thrown? Exception
26+
(let [file (test/known-hosts-file :bad)
27+
session (session-with-options {:known-hosts-file file})]
28+
(session/close session))))
29+
(is (= 0 (count @session/sessions))))
30+
31+
(deftest known-hosts-checking-works-when-the-host-is-known
32+
(is (= 0 (count @session/sessions)))
33+
(let [file (test/known-hosts-file :good)
34+
session (session-with-options {:fail-if-not-in-known-hosts true
35+
:fail-unless-known-hosts-matches true
36+
:known-hosts-file file})]
37+
(is (= 1 (count @session/sessions)))
38+
(session/close session))
39+
(is (= 0 (count @session/sessions))))
40+
41+
(deftest known-host-checking-can-be-ignored
42+
(doseq [known-hosts-file [:good :bad :missing]]
43+
(is (= 0 (count @session/sessions)))
44+
(let [file (test/known-hosts-file known-hosts-file)
45+
session (session-with-options {:fail-if-not-in-known-hosts false
46+
:fail-unless-known-hosts-matches false
47+
:known-hosts-file file})]
48+
(is (= 1 (count @session/sessions)))
49+
(session/close session))
50+
(is (= 0 (count @session/sessions)))))

test/clj_libssh2/test_session.clj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,12 @@
4949
(testing "throws but doesn't crash on authentication failure"
5050
(is (= 0 (count @session/sessions)))
5151
(is (thrown? Exception (open :password "totally-wrong-password")))
52-
(is (= 0 (count @session/sessions)))))
52+
(is (= 0 (count @session/sessions))))
53+
(testing "open rejects bad options"
54+
(is (= 0 (count @session/sessions)))
55+
(is (thrown? AssertionError (session/open test/ssh-host
56+
test/ssh-port
57+
{:username (test/ssh-user)}
58+
{:bad-option :bad-option-value})))
59+
(is (= 0 (count @session/sessions)))
60+
))

test/clj_libssh2/test_utils.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
[script]
1313
(str/join "/" [(System/getProperty "user.dir") "test/script" script]))
1414

15+
(defn known-hosts-file
16+
[variant]
17+
(str/join "/" [(System/getProperty "user.dir") "test/tmp" (str "known_hosts_" (name variant))]))
18+
1519
(defn run-test-script
1620
[script & args]
1721
(let [result (apply sh/sh "sh" (test-script script) (map str args))]

test/script/setup.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,23 @@ ensure_root_dir() {
3737
fi
3838
}
3939

40+
generate_known_hosts_files() {
41+
local host
42+
local port
43+
local good_fingerprint
44+
local bad_fingerprint
45+
46+
host=${1:-127.0.0.1}
47+
port=${2:-2222}
48+
49+
good_fingerprint=$(cut -d' ' -f 1,2 "${TMP_DIR}/ssh_host_rsa_key.pub")
50+
bad_fingerprint=$(echo "${good_fingerprint}" | tr '[:upper:]' '[:lower:]')
51+
52+
echo "[$host]:${port} ${good_fingerprint}" > "${TMP_DIR}/known_hosts_good"
53+
echo "[$host]:${port} ${bad_fingerprint}" > "${TMP_DIR}/known_hosts_bad"
54+
echo "" > "${TMP_DIR}/known_hosts_missing"
55+
}
56+
4057
generate_sshd_host_keys() {
4158
require_executable ssh-keygen
4259
ssh-keygen -N '' -t rsa1 -f "${TMP_DIR}/ssh_host_key" > /dev/null
@@ -80,6 +97,7 @@ launch_sshd() {
8097

8198
require_executable sshd
8299
generate_sshd_host_keys
100+
generate_known_hosts_files "${host}" "${port}"
83101
generate_user_keys
84102
sshd=$(which sshd)
85103
${sshd} -f /dev/null \

0 commit comments

Comments
 (0)