Skip to content

Commit f225e90

Browse files
[JENKINS-73427] With 'Accept first connection' host key verification, and JGit, newly added known_hosts entries are malformed (#1707)
* [JENKINS-73427] With 'Accept first connection' host key verification, and JGit, newly added known_hosts entries are malformed * [JENKINS-73427] Refactor host key verification to avoid malformed entries by removing HashKnownHosts option * [JENKINS-73427] Update test to ensure connection proceeds without crashing on malformed hashed entries * fix formatting violation using mvn spotless:apply * Improve assertions in AcceptFirstConnectionVerifierTest for clarity and accuracy
1 parent 1271b0e commit f225e90

File tree

3 files changed

+101
-6
lines changed

3 files changed

+101
-6
lines changed

src/main/java/org/jenkinsci/plugins/gitclient/verifier/AbstractJGitHostKeyVerifier.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,9 @@ public List<String> getGlobalKnownHostsFiles() {
110110

111111
@Override
112112
public boolean getHashKnownHosts() {
113-
// configurable?
114-
return true;
113+
// Hashing is disabled to avoid issues with malformed entries
114+
// See JENKINS-73427 / https://github.com/jenkinsci/git-client-plugin/issues/1686
115+
return false;
115116
}
116117

117118
@Override

src/main/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifier.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public AbstractCliGitHostKeyVerifier forCliGit(TaskListener listener) {
1313
return tempKnownHosts -> {
1414
listener.getLogger()
1515
.println("Verifying host key using known hosts file, will automatically accept unseen keys");
16-
return "-o StrictHostKeyChecking=accept-new -o HashKnownHosts=yes";
16+
return "-o StrictHostKeyChecking=accept-new";
1717
};
1818
}
1919

src/test/java/org/jenkinsci/plugins/gitclient/verifier/AcceptFirstConnectionVerifierTest.java

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import static org.hamcrest.MatcherAssert.assertThat;
44
import static org.hamcrest.Matchers.containsString;
5+
import static org.hamcrest.Matchers.greaterThan;
56
import static org.hamcrest.Matchers.hasItem;
67
import static org.hamcrest.Matchers.is;
8+
import static org.hamcrest.Matchers.not;
9+
import static org.hamcrest.Matchers.startsWith;
710
import static org.hamcrest.io.FileMatchers.anExistingFile;
811
import static org.jenkinsci.plugins.gitclient.verifier.KnownHostsTestUtil.runKnownHostsTests;
912
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -17,6 +20,7 @@
1720
import java.time.Duration;
1821
import java.util.Collections;
1922
import java.util.List;
23+
import java.util.stream.Collectors;
2024
import org.awaitility.Awaitility;
2125
import org.junit.jupiter.api.BeforeEach;
2226
import org.junit.jupiter.api.Disabled;
@@ -58,7 +62,7 @@ void testVerifyHostKeyOption() throws Exception {
5862
assumeTrue(runKnownHostsTests());
5963
assertThat(
6064
new AcceptFirstConnectionVerifier().forCliGit(TaskListener.NULL).getVerifyHostKeyOption(null),
61-
is("-o StrictHostKeyChecking=accept-new -o HashKnownHosts=yes"));
65+
is("-o StrictHostKeyChecking=accept-new"));
6266
}
6367

6468
@Test
@@ -82,9 +86,99 @@ void testVerifyServerHostKeyWhenFirstConnection() throws Exception {
8286
})
8387
.close();
8488
assertThat(file, is(anExistingFile()));
89+
List<String> lines = Files.readAllLines(file.toPath());
90+
91+
// JENKINS-73427: Verify entries are NOT hashed (no |1| prefix) to avoid malformed entries
92+
assertThat(
93+
"Host key entry should be in plain format, not hashed",
94+
lines.stream().noneMatch(line -> line.startsWith("|1|")),
95+
is(true));
96+
97+
// Verify the key was added with the correct algorithm and key material
98+
assertThat(lines, hasItem(containsString("ecdsa-sha2-nistp256")));
99+
assertThat(
100+
lines,
101+
hasItem(
102+
containsString(
103+
"AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=")));
104+
}
105+
106+
@Test
107+
void testMalformedHashedEntriesCanBeRead() throws Exception {
108+
// JENKINS-73427: Test that malformed hashed entries (from older versions) can still be read
109+
// This is the malformed format that was reported in the issue with TWO comma-separated hash patterns
110+
// The system should log a warning but not crash with an IllegalArgumentException
111+
assumeTrue(runKnownHostsTests());
112+
String malformedHashedEntry = KEY_ECDSA_SHA_2_NISTP_256;
113+
114+
File mockedKnownHosts = knownHostsTestUtil.createFakeKnownHosts(malformedHashedEntry);
115+
AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier());
116+
when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(mockedKnownHosts);
117+
118+
// The connection should proceed without throwing IllegalArgumentException for "Invalid hash pattern"
119+
// The system logs a warning about the malformed entry but continues working
120+
KnownHostsTestUtil.connectToHost(
121+
"github.com",
122+
22,
123+
mockedKnownHosts,
124+
acceptFirstConnectionVerifier.forJGit(StreamBuildListener.fromStdout()),
125+
"ecdsa-sha2-nistp256",
126+
s -> {
127+
assertThat(s.isOpen(), is(true));
128+
Awaitility.await().atMost(Duration.ofSeconds(45)).until(() -> s.getServerKey() != null);
129+
// Successfully connected despite malformed entry in known_hosts
130+
return true;
131+
})
132+
.close();
133+
}
134+
135+
@Test
136+
void testNewEntriesAreNotHashed() throws Exception {
137+
// JENKINS-73427: Verify that new entries are created in plain format, not hashed
138+
// This confirms the fix prevents the malformed hashed entries issue
139+
assumeTrue(runKnownHostsTests());
140+
File file = new File(testFolder + "path/to/newhosts");
141+
AcceptFirstConnectionVerifier acceptFirstConnectionVerifier = spy(new AcceptFirstConnectionVerifier());
142+
when(acceptFirstConnectionVerifier.getKnownHostsFile()).thenReturn(file);
143+
144+
KnownHostsTestUtil.connectToHost(
145+
"github.com",
146+
22,
147+
file,
148+
acceptFirstConnectionVerifier.forJGit(StreamBuildListener.fromStdout()),
149+
"ssh-ed25519",
150+
s -> {
151+
assertThat(s.isOpen(), is(true));
152+
Awaitility.await().atMost(Duration.ofSeconds(45)).until(() -> s.getServerKey() != null);
153+
assertThat(KnownHostsTestUtil.checkKeys(s), is(true));
154+
return true;
155+
})
156+
.close();
157+
158+
assertThat(file, is(anExistingFile()));
159+
List<String> lines = Files.readAllLines(file.toPath());
160+
161+
// Filter out any empty lines
162+
List<String> nonEmptyLines =
163+
lines.stream().filter(line -> !line.trim().isEmpty()).collect(Collectors.toList());
164+
165+
// The key insight: entries should be in plain format like "github.com,140.82.121.4 ssh-ed25519 ..."
166+
// NOT hashed like "|1|hash|hash ssh-ed25519 ..."
167+
assertThat("At least one entry should be created", nonEmptyLines.size(), is(greaterThan(0)));
168+
169+
// Verify ALL entries are NOT hashed (no |1| prefix indicating hashed hostname)
170+
for (String entry : nonEmptyLines) {
171+
assertThat("Entry should not start with |1| (hashed format): " + entry, entry, not(startsWith("|1|")));
172+
173+
// Verify it contains the key type
174+
assertThat("Entry should contain key type: " + entry, entry, containsString("ssh-ed25519"));
175+
}
176+
177+
// Verify at least one entry contains the hostname in plain text
85178
assertThat(
86-
Files.readAllLines(file.toPath()),
87-
hasItem(containsString(KEY_ECDSA_SHA_2_NISTP_256.substring(KEY_ECDSA_SHA_2_NISTP_256.indexOf(" ")))));
179+
"At least one entry should contain plain hostname",
180+
nonEmptyLines.stream().anyMatch(line -> line.contains("github.com")),
181+
is(true));
88182
}
89183

90184
@Test

0 commit comments

Comments
 (0)