22
33import static org .hamcrest .MatcherAssert .assertThat ;
44import static org .hamcrest .Matchers .containsString ;
5+ import static org .hamcrest .Matchers .greaterThan ;
56import static org .hamcrest .Matchers .hasItem ;
67import static org .hamcrest .Matchers .is ;
8+ import static org .hamcrest .Matchers .not ;
9+ import static org .hamcrest .Matchers .startsWith ;
710import static org .hamcrest .io .FileMatchers .anExistingFile ;
811import static org .jenkinsci .plugins .gitclient .verifier .KnownHostsTestUtil .runKnownHostsTests ;
912import static org .junit .jupiter .api .Assumptions .assumeTrue ;
1720import java .time .Duration ;
1821import java .util .Collections ;
1922import java .util .List ;
23+ import java .util .stream .Collectors ;
2024import org .awaitility .Awaitility ;
2125import org .junit .jupiter .api .BeforeEach ;
2226import 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