You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OpenSshServerKeyVerifier.java 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. /*
  2. * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.internal.transport.sshd;
  44. import static java.nio.charset.StandardCharsets.UTF_8;
  45. import static java.text.MessageFormat.format;
  46. import java.io.BufferedReader;
  47. import java.io.BufferedWriter;
  48. import java.io.File;
  49. import java.io.FileNotFoundException;
  50. import java.io.IOException;
  51. import java.io.OutputStreamWriter;
  52. import java.net.InetSocketAddress;
  53. import java.net.SocketAddress;
  54. import java.nio.file.Files;
  55. import java.nio.file.InvalidPathException;
  56. import java.nio.file.Path;
  57. import java.nio.file.Paths;
  58. import java.security.GeneralSecurityException;
  59. import java.security.PublicKey;
  60. import java.util.ArrayList;
  61. import java.util.Collection;
  62. import java.util.Collections;
  63. import java.util.LinkedList;
  64. import java.util.List;
  65. import java.util.Locale;
  66. import java.util.Map;
  67. import java.util.concurrent.ConcurrentHashMap;
  68. import java.util.function.Supplier;
  69. import org.apache.sshd.client.config.hosts.HostConfigEntry;
  70. import org.apache.sshd.client.config.hosts.KnownHostEntry;
  71. import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
  72. import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
  73. import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
  74. import org.apache.sshd.client.session.ClientSession;
  75. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  76. import org.apache.sshd.common.config.keys.KeyUtils;
  77. import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
  78. import org.apache.sshd.common.digest.BuiltinDigests;
  79. import org.apache.sshd.common.util.io.ModifiableFileWatcher;
  80. import org.apache.sshd.common.util.net.SshdSocketAddress;
  81. import org.eclipse.jgit.internal.storage.file.LockFile;
  82. import org.eclipse.jgit.transport.CredentialItem;
  83. import org.eclipse.jgit.transport.CredentialsProvider;
  84. import org.eclipse.jgit.transport.SshConstants;
  85. import org.eclipse.jgit.transport.URIish;
  86. import org.slf4j.Logger;
  87. import org.slf4j.LoggerFactory;
  88. /**
  89. * A sever host key verifier that honors the {@code StrictHostKeyChecking} and
  90. * {@code UserKnownHostsFile} values from the ssh configuration.
  91. * <p>
  92. * The verifier can be given default known_hosts files in the constructor, which
  93. * will be used if the ssh config does not specify a {@code UserKnownHostsFile}.
  94. * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier
  95. * uses the given files in the order given. Non-existing or unreadable files are
  96. * ignored.
  97. * <p>
  98. * {@code StrictHostKeyChecking} accepts the following values:
  99. * </p>
  100. * <dl>
  101. * <dt>ask</dt>
  102. * <dd>Ask the user whether new or changed keys shall be accepted and be added
  103. * to the known_hosts file.</dd>
  104. * <dt>yes/true</dt>
  105. * <dd>Accept only keys listed in the known_hosts file.</dd>
  106. * <dt>no/false</dt>
  107. * <dd>Silently accept all new or changed keys, add new keys to the known_hosts
  108. * file.</dd>
  109. * <dt>accept-new</dt>
  110. * <dd>Silently accept keys for new hosts and add them to the known_hosts
  111. * file.</dd>
  112. * </dl>
  113. * <p>
  114. * If {@code StrictHostKeyChecking} is not set, or set to any other value, the
  115. * default value <b>ask</b> is active.
  116. * </p>
  117. * <p>
  118. * This implementation relies on the {@link ClientSession} being a
  119. * {@link JGitClientSession}. By default Apache MINA sshd does not forward the
  120. * config file host entry to the session, so it would be unknown here which
  121. * entry it was and what setting of {@code StrictHostKeyChecking} should be
  122. * used. If used with some other session type, the implementation assumes
  123. * "<b>ask</b>".
  124. * <p>
  125. * <p>
  126. * Asking the user is done via a {@link CredentialsProvider} obtained from the
  127. * session. If none is set, the implementation falls back to strict host key
  128. * checking ("<b>yes</b>").
  129. * </p>
  130. * <p>
  131. * Note that adding a key to the known hosts file may create the file. You can
  132. * specify in the constructor whether the user shall be asked about that, too.
  133. * If the user declines updating the file, but the key was otherwise
  134. * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are
  135. * active), the key is accepted for this session only.
  136. * </p>
  137. * <p>
  138. * If several known hosts files are specified, a new key is always added to the
  139. * first file (even if it doesn't exist yet; see the note about file creation
  140. * above).
  141. * </p>
  142. *
  143. * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
  144. * ssh-config</a>
  145. */
  146. public class OpenSshServerKeyVerifier
  147. implements ServerKeyVerifier, ServerKeyLookup {
  148. // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
  149. // files may be large!
  150. private static final Logger LOG = LoggerFactory
  151. .getLogger(OpenSshServerKeyVerifier.class);
  152. /** Can be used to mark revoked known host lines. */
  153. private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
  154. private final boolean askAboutNewFile;
  155. private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
  156. private final List<HostKeyFile> defaultFiles = new ArrayList<>();
  157. private enum ModifiedKeyHandling {
  158. DENY, ALLOW, ALLOW_AND_STORE
  159. }
  160. /**
  161. * Creates a new {@link OpenSshServerKeyVerifier}.
  162. *
  163. * @param askAboutNewFile
  164. * whether to ask the user, if possible, about creating a new
  165. * non-existing known_hosts file
  166. * @param defaultFiles
  167. * typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be
  168. * empty or {@code null}, in which case no default files are
  169. * installed. The files need not exist.
  170. */
  171. public OpenSshServerKeyVerifier(boolean askAboutNewFile,
  172. List<Path> defaultFiles) {
  173. if (defaultFiles != null) {
  174. for (Path file : defaultFiles) {
  175. HostKeyFile newFile = new HostKeyFile(file);
  176. knownHostsFiles.put(file, newFile);
  177. this.defaultFiles.add(newFile);
  178. }
  179. }
  180. this.askAboutNewFile = askAboutNewFile;
  181. }
  182. private List<HostKeyFile> getFilesToUse(ClientSession session) {
  183. List<HostKeyFile> filesToUse = defaultFiles;
  184. if (session instanceof JGitClientSession) {
  185. HostConfigEntry entry = ((JGitClientSession) session)
  186. .getHostConfigEntry();
  187. if (entry instanceof JGitHostConfigEntry) {
  188. // Always true!
  189. List<HostKeyFile> userFiles = addUserHostKeyFiles(
  190. ((JGitHostConfigEntry) entry).getMultiValuedOptions()
  191. .get(SshConstants.USER_KNOWN_HOSTS_FILE));
  192. if (!userFiles.isEmpty()) {
  193. filesToUse = userFiles;
  194. }
  195. }
  196. }
  197. return filesToUse;
  198. }
  199. @Override
  200. public List<HostEntryPair> lookup(ClientSession session,
  201. SocketAddress remote) {
  202. List<HostKeyFile> filesToUse = getFilesToUse(session);
  203. HostKeyHelper helper = new HostKeyHelper();
  204. List<HostEntryPair> result = new ArrayList<>();
  205. Collection<SshdSocketAddress> candidates = helper
  206. .resolveHostNetworkIdentities(session, remote);
  207. for (HostKeyFile file : filesToUse) {
  208. for (HostEntryPair current : file.get()) {
  209. KnownHostEntry entry = current.getHostEntry();
  210. for (SshdSocketAddress host : candidates) {
  211. if (entry.isHostMatch(host.getHostName(), host.getPort())) {
  212. result.add(current);
  213. break;
  214. }
  215. }
  216. }
  217. }
  218. return result;
  219. }
  220. @Override
  221. public boolean verifyServerKey(ClientSession clientSession,
  222. SocketAddress remoteAddress, PublicKey serverKey) {
  223. List<HostKeyFile> filesToUse = getFilesToUse(clientSession);
  224. AskUser ask = new AskUser();
  225. HostEntryPair[] modified = { null };
  226. Path path = null;
  227. HostKeyHelper helper = new HostKeyHelper();
  228. for (HostKeyFile file : filesToUse) {
  229. try {
  230. if (find(clientSession, remoteAddress, serverKey, file.get(),
  231. modified, helper)) {
  232. return true;
  233. }
  234. } catch (RevokedKeyException e) {
  235. ask.revokedKey(clientSession, remoteAddress, serverKey,
  236. file.getPath());
  237. return false;
  238. }
  239. if (path == null && modified[0] != null) {
  240. // Remember the file in which we might need to update the
  241. // entry
  242. path = file.getPath();
  243. }
  244. }
  245. if (modified[0] != null) {
  246. // We found an entry, but with a different key
  247. ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
  248. clientSession, remoteAddress, modified[0].getServerKey(),
  249. serverKey, path);
  250. if (toDo == ModifiedKeyHandling.ALLOW_AND_STORE) {
  251. try {
  252. updateModifiedServerKey(clientSession, remoteAddress,
  253. serverKey, modified[0], path, helper);
  254. knownHostsFiles.get(path).resetReloadAttributes();
  255. } catch (IOException e) {
  256. LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
  257. path));
  258. }
  259. }
  260. if (toDo == ModifiedKeyHandling.DENY) {
  261. return false;
  262. }
  263. // TODO: OpenSsh disables password and keyboard-interactive
  264. // authentication in this case. Also agent and local port forwarding
  265. // are switched off. (Plus a few other things such as X11 forwarding
  266. // that are of no interest to a git client.)
  267. return true;
  268. } else if (ask.acceptUnknownKey(clientSession, remoteAddress,
  269. serverKey)) {
  270. if (!filesToUse.isEmpty()) {
  271. HostKeyFile toUpdate = filesToUse.get(0);
  272. path = toUpdate.getPath();
  273. try {
  274. updateKnownHostsFile(clientSession, remoteAddress,
  275. serverKey, path, helper);
  276. toUpdate.resetReloadAttributes();
  277. } catch (IOException e) {
  278. LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
  279. path));
  280. }
  281. }
  282. return true;
  283. }
  284. return false;
  285. }
  286. private static class RevokedKeyException extends Exception {
  287. private static final long serialVersionUID = 1L;
  288. }
  289. private boolean find(ClientSession clientSession,
  290. SocketAddress remoteAddress, PublicKey serverKey,
  291. List<HostEntryPair> entries, HostEntryPair[] modified,
  292. HostKeyHelper helper) throws RevokedKeyException {
  293. Collection<SshdSocketAddress> candidates = helper
  294. .resolveHostNetworkIdentities(clientSession, remoteAddress);
  295. for (HostEntryPair current : entries) {
  296. KnownHostEntry entry = current.getHostEntry();
  297. for (SshdSocketAddress host : candidates) {
  298. if (entry.isHostMatch(host.getHostName(), host.getPort())) {
  299. boolean isRevoked = MARKER_REVOKED
  300. .equals(entry.getMarker());
  301. if (KeyUtils.compareKeys(serverKey,
  302. current.getServerKey())) {
  303. // Exact match
  304. if (isRevoked) {
  305. throw new RevokedKeyException();
  306. }
  307. modified[0] = null;
  308. return true;
  309. } else if (!isRevoked) {
  310. // Server sent a different key
  311. modified[0] = current;
  312. // Keep going -- maybe there's another entry for this
  313. // host
  314. }
  315. }
  316. }
  317. }
  318. return false;
  319. }
  320. private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
  321. if (fileNames == null || fileNames.isEmpty()) {
  322. return Collections.emptyList();
  323. }
  324. List<HostKeyFile> userFiles = new ArrayList<>();
  325. for (String name : fileNames) {
  326. try {
  327. Path path = Paths.get(name);
  328. HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
  329. p -> new HostKeyFile(path));
  330. userFiles.add(file);
  331. } catch (InvalidPathException e) {
  332. LOG.warn(format(SshdText.get().knownHostsInvalidPath,
  333. name));
  334. }
  335. }
  336. return userFiles;
  337. }
  338. private void updateKnownHostsFile(ClientSession clientSession,
  339. SocketAddress remoteAddress, PublicKey serverKey, Path path,
  340. HostKeyHelper updater)
  341. throws IOException {
  342. KnownHostEntry entry = updater.prepareKnownHostEntry(clientSession,
  343. remoteAddress, serverKey);
  344. if (entry == null) {
  345. return;
  346. }
  347. if (!Files.exists(path)) {
  348. if (askAboutNewFile) {
  349. CredentialsProvider provider = getCredentialsProvider(
  350. clientSession);
  351. if (provider == null) {
  352. // We can't ask, so don't create the file
  353. return;
  354. }
  355. URIish uri = new URIish().setPath(path.toString());
  356. if (!askUser(provider, uri, //
  357. format(SshdText.get().knownHostsUserAskCreationPrompt,
  358. path), //
  359. format(SshdText.get().knownHostsUserAskCreationMsg,
  360. path))) {
  361. return;
  362. }
  363. }
  364. }
  365. LockFile lock = new LockFile(path.toFile());
  366. if (lock.lockForAppend()) {
  367. try {
  368. try (BufferedWriter writer = new BufferedWriter(
  369. new OutputStreamWriter(lock.getOutputStream(),
  370. UTF_8))) {
  371. writer.newLine();
  372. writer.write(entry.getConfigLine());
  373. writer.newLine();
  374. }
  375. lock.commit();
  376. } catch (IOException e) {
  377. lock.unlock();
  378. throw e;
  379. }
  380. } else {
  381. LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
  382. path));
  383. }
  384. }
  385. private void updateModifiedServerKey(ClientSession clientSession,
  386. SocketAddress remoteAddress, PublicKey serverKey,
  387. HostEntryPair entry, Path path, HostKeyHelper helper)
  388. throws IOException {
  389. KnownHostEntry hostEntry = entry.getHostEntry();
  390. String oldLine = hostEntry.getConfigLine();
  391. String newLine = helper.prepareModifiedServerKeyLine(clientSession,
  392. remoteAddress, hostEntry, oldLine, entry.getServerKey(),
  393. serverKey);
  394. if (newLine == null || newLine.isEmpty()) {
  395. return;
  396. }
  397. if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) {
  398. // Shouldn't happen.
  399. return;
  400. }
  401. LockFile lock = new LockFile(path.toFile());
  402. if (lock.lock()) {
  403. try {
  404. try (BufferedWriter writer = new BufferedWriter(
  405. new OutputStreamWriter(lock.getOutputStream(), UTF_8));
  406. BufferedReader reader = Files.newBufferedReader(path,
  407. UTF_8)) {
  408. boolean done = false;
  409. String line;
  410. while ((line = reader.readLine()) != null) {
  411. String toWrite = line;
  412. if (!done) {
  413. int pos = line.indexOf('#');
  414. String toTest = pos < 0 ? line
  415. : line.substring(0, pos);
  416. if (toTest.trim().equals(oldLine)) {
  417. toWrite = newLine;
  418. done = true;
  419. }
  420. }
  421. writer.write(toWrite);
  422. writer.newLine();
  423. }
  424. }
  425. lock.commit();
  426. } catch (IOException e) {
  427. lock.unlock();
  428. throw e;
  429. }
  430. } else {
  431. LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
  432. path));
  433. }
  434. }
  435. private static CredentialsProvider getCredentialsProvider(
  436. ClientSession session) {
  437. if (session instanceof JGitClientSession) {
  438. return ((JGitClientSession) session).getCredentialsProvider();
  439. }
  440. return null;
  441. }
  442. private static boolean askUser(CredentialsProvider provider, URIish uri,
  443. String prompt, String... messages) {
  444. List<CredentialItem> items = new ArrayList<>(messages.length + 1);
  445. for (String message : messages) {
  446. items.add(new CredentialItem.InformationalMessage(message));
  447. }
  448. if (prompt != null) {
  449. CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
  450. prompt);
  451. items.add(answer);
  452. return provider.get(uri, items) && answer.getValue();
  453. } else {
  454. return provider.get(uri, items);
  455. }
  456. }
  457. private static class AskUser {
  458. private enum Check {
  459. ASK, DENY, ALLOW;
  460. }
  461. @SuppressWarnings("nls")
  462. private Check checkMode(ClientSession session,
  463. SocketAddress remoteAddress, boolean changed) {
  464. if (!(remoteAddress instanceof InetSocketAddress)) {
  465. return Check.DENY;
  466. }
  467. if (session instanceof JGitClientSession) {
  468. HostConfigEntry entry = ((JGitClientSession) session)
  469. .getHostConfigEntry();
  470. String value = entry.getProperty(
  471. SshConstants.STRICT_HOST_KEY_CHECKING, "ask");
  472. switch (value.toLowerCase(Locale.ROOT)) {
  473. case SshConstants.YES:
  474. case SshConstants.ON:
  475. return Check.DENY;
  476. case SshConstants.NO:
  477. case SshConstants.OFF:
  478. return Check.ALLOW;
  479. case "accept-new":
  480. return changed ? Check.DENY : Check.ALLOW;
  481. default:
  482. break;
  483. }
  484. }
  485. if (getCredentialsProvider(session) == null) {
  486. // This is called only for new, unknown hosts. If we have no way
  487. // to interact with the user, the fallback mode is to deny the
  488. // key.
  489. return Check.DENY;
  490. }
  491. return Check.ASK;
  492. }
  493. public void revokedKey(ClientSession clientSession,
  494. SocketAddress remoteAddress, PublicKey serverKey, Path path) {
  495. CredentialsProvider provider = getCredentialsProvider(
  496. clientSession);
  497. if (provider == null) {
  498. return;
  499. }
  500. InetSocketAddress remote = (InetSocketAddress) remoteAddress;
  501. URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
  502. remote);
  503. String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
  504. serverKey);
  505. String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
  506. String keyAlgorithm = serverKey.getAlgorithm();
  507. askUser(provider, uri, null, //
  508. format(SshdText.get().knownHostsRevokedKeyMsg,
  509. remote.getHostString(), path),
  510. format(SshdText.get().knownHostsKeyFingerprints,
  511. keyAlgorithm),
  512. md5, sha256);
  513. }
  514. public boolean acceptUnknownKey(ClientSession clientSession,
  515. SocketAddress remoteAddress, PublicKey serverKey) {
  516. Check check = checkMode(clientSession, remoteAddress, false);
  517. if (check != Check.ASK) {
  518. return check == Check.ALLOW;
  519. }
  520. CredentialsProvider provider = getCredentialsProvider(
  521. clientSession);
  522. InetSocketAddress remote = (InetSocketAddress) remoteAddress;
  523. // Ask the user
  524. String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
  525. serverKey);
  526. String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
  527. String keyAlgorithm = serverKey.getAlgorithm();
  528. String remoteHost = remote.getHostString();
  529. URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
  530. remote);
  531. String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
  532. return askUser(provider, uri, prompt, //
  533. format(SshdText.get().knownHostsUnknownKeyMsg,
  534. remoteHost),
  535. format(SshdText.get().knownHostsKeyFingerprints,
  536. keyAlgorithm),
  537. md5, sha256);
  538. }
  539. public ModifiedKeyHandling acceptModifiedServerKey(
  540. ClientSession clientSession,
  541. SocketAddress remoteAddress, PublicKey expected,
  542. PublicKey actual, Path path) {
  543. Check check = checkMode(clientSession, remoteAddress, true);
  544. if (check == Check.ALLOW) {
  545. // Never auto-store on CHECK.ALLOW
  546. return ModifiedKeyHandling.ALLOW;
  547. }
  548. InetSocketAddress remote = (InetSocketAddress) remoteAddress;
  549. String keyAlgorithm = actual.getAlgorithm();
  550. String remoteHost = remote.getHostString();
  551. URIish uri = JGitUserInteraction.toURI(clientSession.getUsername(),
  552. remote);
  553. List<String> messages = new ArrayList<>();
  554. String warning = format(
  555. SshdText.get().knownHostsModifiedKeyWarning,
  556. keyAlgorithm, expected.getAlgorithm(), remoteHost,
  557. KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
  558. KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
  559. KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
  560. KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
  561. for (String line : warning.split("\n")) { //$NON-NLS-1$
  562. messages.add(line);
  563. }
  564. CredentialsProvider provider = getCredentialsProvider(
  565. clientSession);
  566. if (check == Check.DENY) {
  567. if (provider != null) {
  568. messages.add(format(
  569. SshdText.get().knownHostsModifiedKeyDenyMsg, path));
  570. askUser(provider, uri, null,
  571. messages.toArray(new String[0]));
  572. }
  573. return ModifiedKeyHandling.DENY;
  574. }
  575. // ASK -- two questions: procceed? and store?
  576. List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
  577. for (String message : messages) {
  578. items.add(new CredentialItem.InformationalMessage(message));
  579. }
  580. CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
  581. SshdText.get().knownHostsModifiedKeyAcceptPrompt);
  582. CredentialItem.YesNoType store = new CredentialItem.YesNoType(
  583. SshdText.get().knownHostsModifiedKeyStorePrompt);
  584. items.add(proceed);
  585. items.add(store);
  586. if (provider.get(uri, items) && proceed.getValue()) {
  587. return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
  588. : ModifiedKeyHandling.ALLOW;
  589. }
  590. return ModifiedKeyHandling.DENY;
  591. }
  592. }
  593. private static class HostKeyFile extends ModifiableFileWatcher
  594. implements Supplier<List<HostEntryPair>> {
  595. private List<HostEntryPair> entries = Collections.emptyList();
  596. public HostKeyFile(Path path) {
  597. super(path);
  598. }
  599. @Override
  600. public List<HostEntryPair> get() {
  601. Path path = getPath();
  602. try {
  603. if (checkReloadRequired()) {
  604. if (!Files.exists(path)) {
  605. // Has disappeared.
  606. resetReloadAttributes();
  607. return Collections.emptyList();
  608. }
  609. LockFile lock = new LockFile(path.toFile());
  610. if (lock.lock()) {
  611. try {
  612. entries = reload(getPath());
  613. } finally {
  614. lock.unlock();
  615. }
  616. } else {
  617. LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
  618. path));
  619. }
  620. }
  621. } catch (IOException e) {
  622. LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
  623. }
  624. return Collections.unmodifiableList(entries);
  625. }
  626. private List<HostEntryPair> reload(Path path) throws IOException {
  627. try {
  628. List<KnownHostEntry> rawEntries = KnownHostEntryReader
  629. .readFromFile(path);
  630. updateReloadAttributes();
  631. if (rawEntries == null || rawEntries.isEmpty()) {
  632. return Collections.emptyList();
  633. }
  634. List<HostEntryPair> newEntries = new LinkedList<>();
  635. for (KnownHostEntry entry : rawEntries) {
  636. AuthorizedKeyEntry keyPart = entry.getKeyEntry();
  637. if (keyPart == null) {
  638. continue;
  639. }
  640. try {
  641. PublicKey serverKey = keyPart.resolvePublicKey(
  642. PublicKeyEntryResolver.IGNORING);
  643. if (serverKey == null) {
  644. LOG.warn(format(
  645. SshdText.get().knownHostsUnknownKeyType,
  646. path, entry.getConfigLine()));
  647. } else {
  648. newEntries.add(new HostEntryPair(entry, serverKey));
  649. }
  650. } catch (GeneralSecurityException e) {
  651. LOG.warn(format(SshdText.get().knownHostsInvalidLine,
  652. path, entry.getConfigLine()));
  653. }
  654. }
  655. return newEntries;
  656. } catch (FileNotFoundException e) {
  657. resetReloadAttributes();
  658. return Collections.emptyList();
  659. }
  660. }
  661. }
  662. // The stuff below is just a hack to avoid having to copy a lot of code from
  663. // KnownHostsServerKeyVerifier
  664. private static class HostKeyHelper extends KnownHostsServerKeyVerifier {
  665. public HostKeyHelper() {
  666. // These two arguments will never be used in any way.
  667. super((c, r, s) -> false, new File(".").toPath()); //$NON-NLS-1$
  668. }
  669. @Override
  670. protected KnownHostEntry prepareKnownHostEntry(
  671. ClientSession clientSession, SocketAddress remoteAddress,
  672. PublicKey serverKey) throws IOException {
  673. // Make this method accessible
  674. try {
  675. return super.prepareKnownHostEntry(clientSession, remoteAddress,
  676. serverKey);
  677. } catch (Exception e) {
  678. throw new IOException(e.getMessage(), e);
  679. }
  680. }
  681. @Override
  682. protected String prepareModifiedServerKeyLine(
  683. ClientSession clientSession, SocketAddress remoteAddress,
  684. KnownHostEntry entry, String curLine, PublicKey expected,
  685. PublicKey actual) throws IOException {
  686. // Make this method accessible
  687. try {
  688. return super.prepareModifiedServerKeyLine(clientSession,
  689. remoteAddress, entry, curLine, expected, actual);
  690. } catch (Exception e) {
  691. throw new IOException(e.getMessage(), e);
  692. }
  693. }
  694. @Override
  695. protected Collection<SshdSocketAddress> resolveHostNetworkIdentities(
  696. ClientSession clientSession, SocketAddress remoteAddress) {
  697. // Make this method accessible
  698. return super.resolveHostNetworkIdentities(clientSession,
  699. remoteAddress);
  700. }
  701. }
  702. }