Skip to content

Commit 74dc742

Browse files
committed
Merge 2.18.0 into dev_3.0.0
Also prepare release documentation.
2 parents 96846b3 + c2d7b7a commit 74dc742

23 files changed

Lines changed: 809 additions & 46 deletions

File tree

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ there are only two bug fixes from 3.0.0-M2.
3434

3535
* [Change notes for 3.0.0-M3](./docs/changes/3.0.0-M3.md)
3636

37+
## Milestone 3: Pre-Release 3.0.0-M4
38+
39+
Merges 2.18.0 into the 3.0.0 stream.
40+
41+
* [Change notes for 3.0.0-M4](./docs/changes/3.0.0-M4.md)
42+
3743
# Planned for the Next Milestone Release
3844

3945
## Bug Fixes

docs/changes/2.18.0.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Introduced in 2.18.0
2+
3+
## Bug Fixes
4+
5+
* [GH-743](https://github.com/apache/mina-sshd/issues/743) Ensure the Java `ServiceLoader` use a singleton `SftpFileSystemProvider`
6+
* [GH-879](https://github.com/apache/mina-sshd/issues/879) Close SSH channel gracefully on exception in port forwarding
7+
* Improve handling of repository paths in `sshd-git`.
8+
9+
## New Features
10+
11+
* [GH-892](https://github.com/apache/mina-sshd/issues/892) Align handling certificates without principals with OpenSSH 10.3
12+
13+
Wildcard principals in host certificates are handled now.
14+
15+
* Putty keys with non-ASCII passphrases
16+
17+
The passphrase needs to be converted to a byte sequence to compute a decryption key for an encrypted private key. This
18+
conversion depends on the character encoding. Putty on Windows uses the ANSI codepage set when the key was generated.
19+
Apache MINA SSHD now tries multiple encodings in sequence: UTF-8, then the OS encoding, and finally ISO-8859-1 as a
20+
last-chance fallback.
21+
22+
## Potential Compatibility Issues
23+
24+
* [GH-892](https://github.com/apache/mina-sshd/issues/892) Align handling certificates without principals with OpenSSH 10.3
25+
26+
OpenSSH 10.3 changed the way such certificates are handled; see the [OpenSSH 10.3 release notes](https://www.openssh.org/txt/release-10.3).
27+
In Apache MINA SSHD, there is a new flag `CoreModuleProperties.ALLOW_EMPTY_CERTIFICATE_PRINCIPALS` (by default `false`)
28+
that can be set on an `SshClient` or `SshServer` or also on a `Session` directly. If the value is `false`, certificates
29+
without principals are rejected as in OpenSSH 10.3; if it is `true`, such certificates are considered to match any
30+
user or host name as in OpenSSH < 10.3.
31+
32+
Set the flag on an `SshClient` or `ClientSession` to determine the handling of host certificates. Set it on an
33+
`SshServer` or `ServerSession` to govern the handling of user certificates.
34+
35+
## Major Code Re-factoring
36+
37+
None.

docs/changes/3.0.0-M3.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This milestone release fixes a minor bug in 3.0.0-M3 and brings in all improvements from the 2.X branch.
44

5-
It includes all the features and bug fixes of [version 2.17.1](./docs/changes/2.17.1.md) and up to
5+
It includes all the features and bug fixes of [version 2.17.1](./2.17.1.md) and up to
66
[commit a3d22510](https://github.com/apache/mina-sshd/blob/a3d22510/CHANGES.md#planned-for-next-version).
77

88
* [Change notes for 3.0.0-M1](./3.0.0-M1.md)

docs/changes/3.0.0-M4.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Milestone Pre-Release 3.0.0-M4
2+
3+
This milestone release just merges the 2.18.0 branch.
4+
5+
It includes all the features and bug fixes of [version 2.18.0](./2.18.0.md).
6+
7+
* [Change notes for 3.0.0-M1](./3.0.0-M1.md)
8+
* [Change notes for 3.0.0-M2](./3.0.0-M2.md)
9+
* [Change notes for 3.0.0-M3](./3.0.0-M3.md)
10+
11+
* For building Apache MINA SSHD 3.0, **Java >= 24** and Apache **Maven >= 3.9.12** are required. Generated artifacts
12+
still use Java 8 as minimum runtime requirement.
13+
14+
## Bug Fixes
15+
16+
None.
17+
18+
## Major Code Re-factoring
19+
20+
None.
21+
22+
## New Features
23+
24+
None.

pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
<required.java.version>[${java.sdk.version},)</required.java.version>
9393

9494
<groovy.version>4.0.17</groovy.version>
95-
<bouncycastle.version>1.83</bouncycastle.version>
95+
<bouncycastle.version>1.84</bouncycastle.version>
9696
<!-- BC FIPS has version numbers 2.x.y. -->
9797
<bouncycastle.upper.bound>3</bouncycastle.upper.bound>
9898
<!-- NOTE: upgrading slf4j beyond this version causes
@@ -109,9 +109,9 @@
109109
<!-- mockito 5.0 requires Java 11. -->
110110
<mockito.version>4.11.0</mockito.version>
111111
<testcontainers.version>2.0.4</testcontainers.version>
112-
<grpc.version>1.78.0</grpc.version> <!-- Used only in tests -->
112+
<grpc.version>1.79.0</grpc.version> <!-- Used only in tests -->
113113

114-
<maven.archiver.version>3.6.5</maven.archiver.version>
114+
<maven.archiver.version>3.6.6</maven.archiver.version>
115115
<plexus.archiver.version>4.11.0</plexus.archiver.version>
116116
<!-- See https://pmd.github.io/ for available latest version -->
117117
<pmd.version>7.22.0</pmd.version>
@@ -803,7 +803,7 @@
803803
<dependency>
804804
<groupId>commons-io</groupId>
805805
<artifactId>commons-io</artifactId>
806-
<version>2.20.0</version>
806+
<version>2.21.0</version>
807807
</dependency>
808808
</dependencies>
809809
</plugin>
@@ -1017,7 +1017,7 @@
10171017
<plugin>
10181018
<groupId>net.revelc.code</groupId>
10191019
<artifactId>impsort-maven-plugin</artifactId>
1020-
<version>1.12.0</version>
1020+
<version>1.13.0</version>
10211021
<configuration>
10221022
<lineEnding>LF</lineEnding>
10231023
<groups>java.,javax.,org.w3c.,org.xml.,junit.</groups>

sshd-core/src/main/java/org/apache/sshd/client/kex/DHGClient.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.Arrays;
2525
import java.util.Collection;
2626
import java.util.Objects;
27+
import java.util.regex.Matcher;
28+
import java.util.regex.Pattern;
2729

2830
import org.apache.sshd.client.session.AbstractClientSession;
2931
import org.apache.sshd.common.NamedFactory;
@@ -257,11 +259,15 @@ protected void verifyCertificate(Session session, OpenSshCertificate openSshKey)
257259
"KeyExchange CA signature verification failed for key type=" + keyAlg + " of key ID=" + keyId);
258260
}
259261

262+
// OpenSSH < 10.3:
263+
//
260264
// "As a special case, a zero-length "valid principals" field means the certificate is valid for
261265
// any principal of the specified type."
262266
// See https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys
263267
//
264268
// Empty principals in a host certificate mean the certificate is valid for any host.
269+
//
270+
// OpenSSH >= 10.3: such certificates are never valid.
265271
Collection<String> principals = openSshKey.getPrincipals();
266272
if (!GenericUtils.isEmpty(principals)) {
267273
/*
@@ -275,7 +281,7 @@ protected void verifyCertificate(Session session, OpenSshCertificate openSshKey)
275281

276282
if (connectSocketAddress instanceof InetSocketAddress) {
277283
String hostName = ((InetSocketAddress) connectSocketAddress).getHostString();
278-
if (GenericUtils.isEmpty(principals) || (!principals.contains(hostName))) {
284+
if (!hostMatches(principals, hostName)) {
279285
throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
280286
"KeyExchange signature verification failed, invalid principal " + hostName + " for key ID=" + keyId
281287
+ " - allowed=" + principals);
@@ -284,6 +290,9 @@ protected void verifyCertificate(Session session, OpenSshCertificate openSshKey)
284290
throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
285291
"KeyExchange signature verification failed, could not determine connect host for key ID=" + keyId);
286292
}
293+
} else if (!CoreModuleProperties.ALLOW_EMPTY_CERTIFICATE_PRINCIPALS.getRequired(session)) {
294+
throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
295+
"KeyExchange signature verification failed because the certificate has no principals; key ID=" + keyId);
287296
}
288297

289298
if (!openSshKey.getCriticalOptions().isEmpty()) {
@@ -295,4 +304,18 @@ protected void verifyCertificate(Session session, OpenSshCertificate openSshKey)
295304
+ keyId);
296305
}
297306
}
307+
308+
private Pattern wildcardToRegex(String wildcardPattern) {
309+
String re = "^\\Q" + wildcardPattern + "\\E$";
310+
re = re.replace("?", "\\E.\\Q").replaceAll("\\*+", Matcher.quoteReplacement("\\E.*?\\Q")).replace("\\Q\\E", "");
311+
return Pattern.compile(re);
312+
}
313+
314+
private boolean hostMatches(Collection<String> principals, String hostName) {
315+
return principals.stream() //
316+
.filter(s -> s != null && !s.isEmpty()) //
317+
.anyMatch(principal -> (principal.contains("?") || principal.contains("*"))
318+
? wildcardToRegex(principal).matcher(hostName).matches()
319+
: principal.equals(hostName));
320+
}
298321
}

sshd-core/src/main/java/org/apache/sshd/client/keyverifier/KnownHostsServerKeyVerifier.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,18 @@ protected boolean acceptKnownHostEntries(
298298
.filter(e -> isCert == "cert-authority".equals(e.getHostEntry().getMarker()))
299299
.collect(Collectors.toList());
300300
if (!keyMatches.isEmpty()) {
301+
if (serverKey instanceof OpenSshCertificate) {
302+
// Also check whether the certified key has been revoked.
303+
PublicKey certifiedKey = ((OpenSshCertificate) serverKey).getCertPubKey();
304+
String certKeyType = KeyUtils.getKeyType(certifiedKey);
305+
if (hostMatches.stream() //
306+
.filter(entry -> certKeyType.equals(entry.getHostEntry().getKeyEntry().getKeyType()))
307+
.filter(k -> KeyUtils.compareKeys(k.getServerKey(), certifiedKey))
308+
.anyMatch(entry -> "revoked".equals(entry.getHostEntry().getMarker()))) {
309+
handleRevokedKey(clientSession, remoteAddress, certifiedKey);
310+
return false;
311+
}
312+
}
301313
return true;
302314
}
303315

@@ -306,7 +318,8 @@ protected boolean acceptKnownHostEntries(
306318
}
307319

308320
Optional<HostEntryPair> anyNonRevokedMatch = hostMatches.stream()
309-
.filter(k -> !"revoked".equals(k.getHostEntry().getMarker()))
321+
.filter(k -> !"revoked".equals(k.getHostEntry().getMarker())
322+
&& !"cert-authority".equals(k.getHostEntry().getMarker()))
310323
.findAny();
311324

312325
if (!anyNonRevokedMatch.isPresent()) {

sshd-core/src/main/java/org/apache/sshd/core/CoreModuleProperties.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,12 @@ public final class CoreModuleProperties {
780780
}
781781
});
782782

783+
/**
784+
* Whether to allow SSH user or host certificates without principals. OpenSSH < 10.3 considered such certificates to
785+
* be valid for any principal; since OpenSSH 10.3 such certificates are rejected.
786+
*/
787+
public static final Property<Boolean> ALLOW_EMPTY_CERTIFICATE_PRINCIPALS = Property.bool("empty-cert-principals", false);
788+
783789
public static final int DEFAULT_MAX_MSGS_BEFORE_KEX_INIT = 1_000;
784790

785791
/**

sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/AuthorizedKeyEntriesPublickeyAuthenticator.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.apache.sshd.common.util.GenericUtils;
3737
import org.apache.sshd.common.util.MapEntryUtils;
3838
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
39+
import org.apache.sshd.core.CoreModuleProperties;
3940
import org.apache.sshd.server.session.ServerSession;
4041

4142
/**
@@ -125,14 +126,18 @@ protected boolean matchesPrincipals(
125126
AuthorizedKeyEntry entry, String username, OpenSshCertificate cert,
126127
ServerSession session) {
127128
Collection<String> certPrincipals = cert.getPrincipals();
129+
// OpenSSH < 10.3:
130+
//
131+
// "As a special case, a zero-length "valid principals" field means the certificate is valid for
132+
// any principal of the specified type."
133+
// See https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys
134+
//
135+
// This is true for user certificates unless they are checked via a TrustedUserCAKeys file, but
136+
// that is not what we implement here.
137+
// See https://man.openbsd.org/sshd_config#TrustedUserCAKeys
138+
//
139+
// OpenSSH >= 10.3: certificates without principals never match
128140
if (!GenericUtils.isEmpty(certPrincipals)) {
129-
// "As a special case, a zero-length "valid principals" field means the certificate is valid for
130-
// any principal of the specified type."
131-
// See https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.certkeys
132-
//
133-
// This is true for user certificates unless they are checked via a TrustedUserCAKeys file, but
134-
// that is not what we implement here.
135-
// See https://man.openbsd.org/sshd_config#TrustedUserCAKeys
136141
String allowedPrincipals = entry.getLoginOptions().get("principals");
137142
if (!GenericUtils.isEmpty(allowedPrincipals)) {
138143
if (Stream.of(allowedPrincipals.split(",")) //
@@ -146,12 +151,16 @@ protected boolean matchesPrincipals(
146151
} else {
147152
// We have a match for the certificate, but no principals from the entry: check that given
148153
// user name is in the certificate's principals.
149-
if (!GenericUtils.isEmpty(certPrincipals) && !certPrincipals.contains(username)) {
154+
if (!certPrincipals.contains(username)) {
150155
log.debug("authenticate({})[{}] certificate match rejected, user not in certificate principals: {}",
151-
username, session, username);
156+
username, session, certPrincipals);
152157
return false;
153158
}
154159
}
160+
} else if (!CoreModuleProperties.ALLOW_EMPTY_CERTIFICATE_PRINCIPALS.getRequired(session)) {
161+
log.debug("authenticate({})[{}] certificate match rejected because the certificate has no principals", username,
162+
session);
163+
return false;
155164
}
156165
return true;
157166
}

sshd-core/src/test/java/org/apache/sshd/common/signature/KnownHostsCertificateTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import java.nio.file.Files;
2222
import java.nio.file.Path;
23+
import java.nio.file.StandardOpenOption;
2324
import java.security.KeyPair;
2425
import java.util.Arrays;
2526
import java.util.Collections;
@@ -37,16 +38,19 @@
3738
import org.apache.sshd.common.config.keys.PublicKeyEntry;
3839
import org.apache.sshd.common.keyprovider.KeyPairProvider;
3940
import org.apache.sshd.common.util.GenericUtils;
41+
import org.apache.sshd.core.CoreModuleProperties;
4042
import org.apache.sshd.server.SshServer;
4143
import org.apache.sshd.util.test.BaseTestSupport;
4244
import org.apache.sshd.util.test.CommonTestSupportUtils;
4345
import org.apache.sshd.util.test.CoreTestSupportUtils;
4446
import org.junit.jupiter.api.AfterAll;
47+
import org.junit.jupiter.api.AfterEach;
4548
import org.junit.jupiter.api.BeforeAll;
4649
import org.junit.jupiter.api.Test;
4750
import org.junit.jupiter.api.io.TempDir;
4851
import org.junit.jupiter.params.ParameterizedTest;
4952
import org.junit.jupiter.params.provider.MethodSource;
53+
import org.junit.jupiter.params.provider.ValueSource;
5054

5155
/**
5256
* Tests for KEX with host certificates with host key validation through a {@link KnownHostsServerKeyVerifier}.
@@ -92,6 +96,11 @@ static void tearDownClientAndServer() throws Exception {
9296
}
9397
}
9498

99+
@AfterEach
100+
void resetCertificateProperty() {
101+
CoreModuleProperties.ALLOW_EMPTY_CERTIFICATE_PRINCIPALS.set(client, false);
102+
}
103+
95104
private static Stream<String> markers() {
96105
return Stream.of("rejected", "", null);
97106
}
@@ -155,8 +164,22 @@ void testHostCertificateSucceeds() throws Exception {
155164
}
156165
}
157166

167+
@Test
168+
void testHostCertificateWithoutPrincipalsFails() throws Exception {
169+
initKeys(KeyUtils.EC_ALGORITHM, 256, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256", "cert-authority",
170+
new String[0]);
171+
assertThrows(SshException.class, () -> {
172+
try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(CONNECT_TIMEOUT)
173+
.getSession()) {
174+
s.addPasswordIdentity(getCurrentTestName());
175+
s.auth().verify(AUTH_TIMEOUT);
176+
}
177+
});
178+
}
179+
158180
@Test
159181
void testHostCertificateWithoutPrincipalsSucceeds() throws Exception {
182+
CoreModuleProperties.ALLOW_EMPTY_CERTIFICATE_PRINCIPALS.set(client, true);
160183
initKeys(KeyUtils.EC_ALGORITHM, 256, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256", "cert-authority",
161184
new String[0]);
162185
try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(CONNECT_TIMEOUT)
@@ -165,4 +188,36 @@ void testHostCertificateWithoutPrincipalsSucceeds() throws Exception {
165188
s.auth().verify(AUTH_TIMEOUT);
166189
}
167190
}
191+
192+
@ParameterizedTest(name = "test CA key with {0}")
193+
@ValueSource(strings = { "loca?host,127.0.0.?", "loca*ost,127.?.?.*" })
194+
void testHostCertificateWithWildcardSucceeds(String principals) throws Exception {
195+
initKeys(KeyUtils.EC_ALGORITHM, 256, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256", "cert-authority",
196+
principals.split(","));
197+
try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(CONNECT_TIMEOUT)
198+
.getSession()) {
199+
s.addPasswordIdentity(getCurrentTestName());
200+
s.auth().verify(AUTH_TIMEOUT);
201+
}
202+
}
203+
204+
@Test
205+
void testHostCertificateWithRejectedHostKeyFails() throws Exception {
206+
initKeys(KeyUtils.EC_ALGORITHM, 256, KeyUtils.EC_ALGORITHM, 256, "ecdsa-sha2-nistp256", "cert-authority");
207+
Path knownHosts = tmp.resolve("known_hosts");
208+
StringBuilder line = new StringBuilder();
209+
line.append("@revoked ");
210+
line.append("[localhost]:").append(port).append(",[127.0.0.1]:").append(port).append(' ');
211+
line.append(PublicKeyEntry.toString(hostKey.getPublic()));
212+
line.append('\n');
213+
Files.write(knownHosts, Collections.singletonList(line.toString()), StandardOpenOption.APPEND);
214+
client.setServerKeyVerifier(new KnownHostsServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE, knownHosts));
215+
assertThrows(SshException.class, () -> {
216+
try (ClientSession s = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(CONNECT_TIMEOUT)
217+
.getSession()) {
218+
s.addPasswordIdentity(getCurrentTestName());
219+
s.auth().verify(AUTH_TIMEOUT);
220+
}
221+
});
222+
}
168223
}

0 commit comments

Comments
 (0)