This is an automated email from the ASF dual-hosted git repository. tomaswolf pushed a commit to branch rc-3.0.0-M5 in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 942554a9d8a177aade549eea81aead656a02e2a0 Merge: 20c2f4f43 e94075277 Author: Thomas Wolf <[email protected]> AuthorDate: Tue Jun 30 00:29:30 2026 +0200 Merge branch 'master' into dev_3.0 .asf.yaml | 2 +- .github/workflows/master-build.yml | 6 +- NOTICE-bin.txt | 2 +- docs/changes/2.18.0.md | 2 +- docs/server-setup.md | 6 +- .../common/config/keys/OpenSshCertificate.java | 16 ++ .../signature/AbstractSecurityKeySignature.java | 3 +- .../java/org/apache/sshd/common/util/OsUtils.java | 2 +- .../sshd/agent/common/AbstractAgentClient.java | 7 +- .../sshd/agent/common/AbstractAgentProxy.java | 28 ++- .../sshd/client/auth/pubkey/UserAuthPublicKey.java | 7 +- .../sshd/server/auth/pubkey/UserAuthPublicKey.java | 22 +- .../shell/InteractiveProcessShellFactory.java | 10 +- .../sshd/server/shell/ProcessShellFactory.java | 39 ++-- .../java/org/apache/sshd/agent/AgentUnitTest.java | 94 ++++++++ .../auth/pubkey/UserAuthPublicKeySkTest.java | 125 ++++++++++ .../auth/UserAuthPublicKeyCertificateTest.java | 241 +++++++++++++++++++ .../apache/sshd/server/shell/ProcessShellTest.java | 28 ++- sshd-git/README.txt | 21 +- .../org/apache/sshd/git/AbstractGitCommand.java | 22 +- .../org/apache/sshd/git/GitModuleProperties.java | 9 + .../apache/sshd/git/pgm/EmbeddedCommandRunner.java | 77 +++++- .../org/apache/sshd/git/pgm/GitPgmCommand.java | 5 +- .../org/apache/sshd/git/pgm/SubcommandHandler.java | 61 +++++ .../apache/sshd/git/AbstractGitCommandTest.java | 51 ++++ .../apache/sshd/git/pack/GitPackCommandTest.java | 2 +- .../org/apache/sshd/git/pgm/GitPgmCommandTest.java | 102 ++++---- .../org/apache/sshd/scp/common/ScpFileOpener.java | 37 ++- .../java/org/apache/sshd/scp/common/ScpHelper.java | 12 +- .../helpers/LocalFileScpTargetStreamResolver.java | 10 +- .../helpers/ScpReceivePathTraversalTest.java | 122 ++++++++++ .../apache/sshd/client/auth/SkPubKeyAuthTest.java | 260 +++++++++++++++++++++ 32 files changed, 1323 insertions(+), 108 deletions(-) diff --cc docs/changes/2.18.0.md index 7a3aa9592,b08e3ac2c..5564e9985 --- a/docs/changes/2.18.0.md +++ b/docs/changes/2.18.0.md @@@ -4,7 -4,7 +4,7 @@@ * [GH-743](https://github.com/apache/mina-sshd/issues/743) Ensure the Java `ServiceLoader` use a singleton `SftpFileSystemProvider` * [GH-879](https://github.com/apache/mina-sshd/issues/879) Close SSH channel gracefully on exception in port forwarding - * Improve handling of repository paths in `sshd-git`. -* **Security**: Improve handling of repository paths in `sshd-git`. Resolves [CVE-2026-48827](https://www.cve.org/CVERecord?id=CVE-2026-48827), [announced](https://lists.apache.org/thread/910kq9ghm6js0k1yhhbrdm9sf5tqq9c9) 2026-05-30. ++**Security**: Improve handling of repository paths in `sshd-git`. Resolves [CVE-2026-48827](https://www.cve.org/CVERecord?id=CVE-2026-48827), [announced](https://lists.apache.org/thread/910kq9ghm6js0k1yhhbrdm9sf5tqq9c9) 2026-05-30. ## New Features diff --cc sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java index a732bb382,9b4b4b40d..aa3aacccc --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java @@@ -202,8 -218,25 +203,25 @@@ public class UserAuthPublicKey extends } } + protected void verifyCriticalOptions(ServerSession session, OpenSshCertificate cert) throws CertificateException { - Map<String, String> criticalOptions = cert.getCriticalOptionsMap(); ++ Map<String, String> criticalOptions = cert.getCriticalOptions(); + for (String option : criticalOptions.keySet()) { + if (OpenSshCertificate.VERIFY_REQUIRED.equals(option)) { + throw new CertificateException( + "Rejected by verify-required critical option: not implemented yet, have certificate type " + + cert.getKeyType()); + } else if (OpenSshCertificate.SOURCE_ADDRESS.equals(option)) { + verifyCertificateSources(session, cert); + } else if (OpenSshCertificate.FORCE_COMMAND.equals(option)) { + throw new CertificateException("Rejected by force-command critical option: not implemented yet"); + } else { + throw new CertificateException("Rejected by unknown critical option " + option); + } + } + } + protected void verifyCertificateSources(ServerSession session, OpenSshCertificate cert) throws CertificateException { - String allowedSources = cert.getCriticalOptions().get("source-address"); - String allowedSources = cert.getCriticalOptionsMap().get(OpenSshCertificate.SOURCE_ADDRESS); ++ String allowedSources = cert.getCriticalOptions().get(OpenSshCertificate.SOURCE_ADDRESS); if (allowedSources == null) { return; } diff --cc sshd-core/src/test/java/org/apache/sshd/agent/AgentUnitTest.java index 1ee78298d,951b1bcc9..841021b34 --- a/sshd-core/src/test/java/org/apache/sshd/agent/AgentUnitTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/agent/AgentUnitTest.java @@@ -70,6 -93,46 +79,46 @@@ class AgentUnitTest extends BaseTestSup assertTrue(verifier.verify(null, signature), "Signature should validate"); } + @Test - public void securityKeySignatureBlob() throws Exception { ++ void securityKeySignatureBlob() throws Exception { + KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC"); + generator.initialize(256); + KeyPair pair = generator.generateKeyPair(); + ECPublicKey ecPublicKey = ValidateUtils.checkInstanceOf( + pair.getPublic(), ECPublicKey.class, "Expected an ECPublicKey"); + SkEcdsaPublicKey key = new SkEcdsaPublicKey("ssh", false, false, ecPublicKey); + + byte[] rawSignature = { 1, 2, 3, 4, 5 }; + byte flags = 1; + long counter = 42; + byte[] signature = createSkSignature(rawSignature, flags, counter); + + SshAgent agent = new StaticSignatureAgent(key, signature); + Server server = new Server(agent); + Client client = new Client(server); + server.setClient(client); + + Map.Entry<String, byte[]> result = client.sign(null, key, key.getKeyType(), new byte[] { 'd', 'a', 't', 'a' }); + assertEquals(key.getKeyType(), result.getKey(), "Unexpected signature algorithm"); + assertSkSignature(rawSignature, flags, counter, result.getValue()); + } + + private static byte[] createSkSignature(byte[] rawSignature, byte flags, long counter) { + ByteArrayBuffer buffer = new ByteArrayBuffer(); + buffer.putBytes(rawSignature); + buffer.putByte(flags); + buffer.putUInt(counter); + return buffer.getCompactData(); + } + + private static void assertSkSignature(byte[] rawSignature, byte flags, long counter, byte[] signature) { + ByteArrayBuffer buffer = new ByteArrayBuffer(signature); + assertArrayEquals(rawSignature, buffer.getBytes()); + assertEquals(flags, buffer.getByte()); + assertEquals(counter, buffer.getUInt()); + assertEquals(0, buffer.available()); + } + private static class Server extends AbstractAgentClient { private Client client; diff --cc sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeyCertificateTest.java index 000000000,066e15518..f4ba2c9a4 mode 000000,100644..100644 --- a/sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeyCertificateTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeyCertificateTest.java @@@ -1,0 -1,234 +1,241 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + package org.apache.sshd.server.auth; + + import java.net.InetAddress; + import java.net.InetSocketAddress; + import java.security.KeyPair; + import java.security.KeyPairGenerator; + import java.security.PrivateKey; + import java.security.SignatureException; + import java.util.Arrays; + import java.util.Collections; + import java.util.concurrent.ThreadLocalRandom; + + import org.apache.sshd.certificate.OpenSshCertificateBuilder; + import org.apache.sshd.common.Factory; + import org.apache.sshd.common.SshConstants; + import org.apache.sshd.common.auth.AbstractUserAuthServiceFactory; + import org.apache.sshd.common.config.keys.OpenSshCertificate; + import org.apache.sshd.common.io.IoSession; + import org.apache.sshd.common.random.JceRandomFactory; + import org.apache.sshd.common.random.Random; + import org.apache.sshd.common.random.SingletonRandomFactory; + import org.apache.sshd.common.signature.BuiltinSignatures; + import org.apache.sshd.common.signature.Signature; + import org.apache.sshd.common.util.buffer.BufferUtils; + import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + import org.apache.sshd.common.util.security.SecurityUtils; + import org.apache.sshd.server.ServerFactoryManager; + import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator; + import org.apache.sshd.server.auth.pubkey.UserAuthPublicKey; + import org.apache.sshd.server.session.ServerSessionImpl; + import org.apache.sshd.util.test.BaseTestSupport; + import org.junit.jupiter.api.Tag; + import org.junit.jupiter.params.ParameterizedTest; + import org.junit.jupiter.params.provider.CsvSource; + import org.mockito.Mockito; + + /** + * Unit test for {@link UserAuthPublickey} handling certificates on the server-side. + */ + @Tag("NoIoTestCase") + class UserAuthPublicKeyCertificateTest extends BaseTestSupport { + + @ParameterizedTest(name = "auth {0} {1}") + @CsvSource({ // + ",,true", // + "source-address,10.1.2.3/32,true", // + "source-address,10.1/16,true", // + "source-address,'10.2/16,10.1/16', true", // + "source-address,10.2/16,false", // + "source-address,10.1.2.1/32,false", // + "source-address,172.1/16,false", // + "verify-required,,false", // + "verify-required,bogus,false", // + "force-command,,false", // + "force-command,exit,false", // + "unknown-option,,false", // + "unknown-option,unknown,false" // + }) + void testCertOptions(String criticalOption, String optionValue, boolean expectSuccess) throws Exception { + KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC"); + generator.initialize(256); + KeyPair pair = generator.generateKeyPair(); + + // Create a CA key + KeyPair caKeypair = generator.generateKeyPair(); + // Create a certificate for the above key + OpenSshCertificateBuilder builder = OpenSshCertificateBuilder.userCertificate() // + .serial(0L) // + .publicKey(pair.getPublic()) // + .id("testuser") // + .principals(Collections.singletonList("testuser")); + if (criticalOption != null && !criticalOption.isEmpty()) { + builder = builder.criticalOption(criticalOption, optionValue); + } + OpenSshCertificate cert = builder.sign(caKeypair); + + MockSession session = createSession(); + // Give it a session ID since it is part of the signed data. + byte[] id = new byte[32]; + ThreadLocalRandom.current().nextBytes(id); + session.setSessionId(id); + // We don't care about the authenticator in this test. + session.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE); + // Create the UserAuthPublickey object under test; give it the signature factories we need. + UserAuthPublicKey pubkeyAuth = new UserAuthPublicKey( + Arrays.asList(BuiltinSignatures.nistp256_cert, BuiltinSignatures.nistp256)); + + // Create a buffer with a full properly signed authentication request. + ByteArrayBuffer buffer = createRequest(id, cert, pair.getPrivate()); + + String userName = buffer.getString(); + String serviceName = buffer.getString(); + buffer.getString(); // Skip method name + + // Finally try to authenticate and check the result. + try { + Boolean result = pubkeyAuth.auth(session, userName, serviceName, buffer); + assertEquals(expectSuccess, result.booleanValue()); + } catch (SignatureException e) { + if (!"Key verification failed".equals(e.getMessage())) { + throw e; + } + assertFalse(expectSuccess); + } + } + + @ParameterizedTest(name = "auth {0} {1}") + @CsvSource({ // + ",,true", // + "verify-required,,false", // + "verify-required,bogus,false", // + "force-command,,false", // + "force-command,exit,false", // + "unknown-option,,false", // + "unknown-option,unknown,false" // + }) + void testMultipleCertOptions(String criticalOption, String optionValue, boolean expectSuccess) throws Exception { + KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC"); + generator.initialize(256); + KeyPair pair = generator.generateKeyPair(); + + // Create a CA key + KeyPair caKeypair = generator.generateKeyPair(); + // Create a certificate for the above key + OpenSshCertificateBuilder builder = OpenSshCertificateBuilder.userCertificate() // + .serial(0L) // + .publicKey(pair.getPublic()) // + .id("testuser") // + .principals(Collections.singletonList("testuser")); + builder.criticalOption(OpenSshCertificate.SOURCE_ADDRESS, "10.1.2.3/24"); + if (criticalOption != null && !criticalOption.isEmpty()) { + builder = builder.criticalOption(criticalOption, optionValue); + } + OpenSshCertificate cert = builder.sign(caKeypair); + + MockSession session = createSession(); + // Give it a session ID since it is part of the signed data. + byte[] id = new byte[32]; + ThreadLocalRandom.current().nextBytes(id); + session.setSessionId(id); + // We don't care about the authenticator in this test. + session.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE); + // Create the UserAuthPublickey object under test; give it the signature factories we need. + UserAuthPublicKey pubkeyAuth = new UserAuthPublicKey( + Arrays.asList(BuiltinSignatures.nistp256_cert, BuiltinSignatures.nistp256)); + + // Create a buffer with a full properly signed authentication request. + ByteArrayBuffer buffer = createRequest(id, cert, pair.getPrivate()); + + String userName = buffer.getString(); + String serviceName = buffer.getString(); + buffer.getString(); // Skip method name + + // Finally try to authenticate and check the result. + try { + Boolean result = pubkeyAuth.auth(session, userName, serviceName, buffer); + assertEquals(expectSuccess, result.booleanValue()); + } catch (SignatureException e) { + if (!"Key verification failed".equals(e.getMessage())) { + throw e; + } + assertFalse(expectSuccess); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private MockSession createSession() throws Exception { + // Create a mock ServerSession. We can't simply mock the server session since the authenticator might want to + // set an attribute on it. + ServerFactoryManager manager = Mockito.mock(ServerFactoryManager.class); + Factory<? extends Random> randomFactory = new SingletonRandomFactory(JceRandomFactory.INSTANCE); + Mockito.when(manager.getRandomFactory()).thenReturn((Factory) randomFactory); + IoSession ioSession = Mockito.mock(IoSession.class); + InetSocketAddress fakeClientAddress = new InetSocketAddress(InetAddress.getByAddress(new byte[] { 10, 1, 2, 3 }), 1234); + Mockito.when(ioSession.getRemoteAddress()).thenReturn(fakeClientAddress); + return new MockSession(manager, ioSession); + } + + private ByteArrayBuffer createRequest(byte[] sessionId, OpenSshCertificate cert, PrivateKey priv) + throws Exception { + ByteArrayBuffer payload = new ByteArrayBuffer(); + payload.putString("testuser"); + payload.putString(AbstractUserAuthServiceFactory.DEFAULT_NAME); + payload.putString(UserAuthPublicKey.NAME); + payload.putBoolean(true); // With signature + payload.putString(cert.getKeyType()); // Algorithm + payload.putPublicKey(cert); + + Signature signer = BuiltinSignatures.nistp256_cert.create(); + signer.initSigner(null, priv); + byte[] uint = new byte[4]; + BufferUtils.putUInt(sessionId.length, uint); + signer.update(null, uint); + signer.update(null, sessionId); + signer.update(null, new byte[] { SshConstants.SSH_MSG_USERAUTH_REQUEST }); + signer.update(null, payload.getCompactData()); + byte[] rawSignature = signer.sign(null); + + ByteArrayBuffer sig = new ByteArrayBuffer(); + sig.putString(cert.getRawKeyType()); + sig.putBytes(rawSignature); + payload.putBytes(sig.getCompactData()); + return payload; + } + + private static class MockSession extends ServerSessionImpl { + ++ private byte[] sessionId; ++ + MockSession(ServerFactoryManager server, IoSession ioSession) throws Exception { + super(server, ioSession); + } + + void setSessionId(byte[] id) { + sessionId = id.clone(); + } ++ ++ @Override ++ public byte[] getSessionId() { ++ return sessionId; ++ } + } + } diff --cc sshd-test/src/test/java/org/apache/sshd/client/auth/SkPubKeyAuthTest.java index 000000000,c627a0fbe..5787b645d mode 000000,100644..100644 --- a/sshd-test/src/test/java/org/apache/sshd/client/auth/SkPubKeyAuthTest.java +++ b/sshd-test/src/test/java/org/apache/sshd/client/auth/SkPubKeyAuthTest.java @@@ -1,0 -1,260 +1,260 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ -package org.apache.sshd.client.auth.pubkey; ++package org.apache.sshd.client.auth; + + import java.io.File; + import java.nio.charset.StandardCharsets; + import java.nio.file.Files; + import java.nio.file.Path; + import java.security.GeneralSecurityException; + import java.security.KeyPair; + import java.security.KeyPairGenerator; + import java.security.PrivateKey; + import java.security.interfaces.ECPublicKey; + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + + import org.apache.sshd.client.SshClient; + import org.apache.sshd.client.session.ClientSession; + import org.apache.sshd.common.NamedFactory; + import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; + import org.apache.sshd.common.config.keys.PublicKeyEntry; + import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; + import org.apache.sshd.common.config.keys.u2f.SkEcdsaPublicKey; + import org.apache.sshd.common.keyprovider.KeyPairProvider; + import org.apache.sshd.common.session.SessionContext; + import org.apache.sshd.common.signature.BuiltinSignatures; + import org.apache.sshd.common.signature.Signature; + import org.apache.sshd.common.signature.SignatureSkECDSA; + import org.apache.sshd.common.util.ValidateUtils; + import org.apache.sshd.common.util.buffer.BufferUtils; + import org.apache.sshd.common.util.buffer.ByteArrayBuffer; + import org.apache.sshd.common.util.security.SecurityUtils; + import org.apache.sshd.server.SshServer; + import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; + import org.apache.sshd.util.test.BaseTestSupport; + import org.apache.sshd.util.test.CoreTestSupportUtils; + import org.junit.jupiter.api.Assumptions; + import org.junit.jupiter.api.Test; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + import org.testcontainers.DockerClientFactory; + import org.testcontainers.containers.GenericContainer; + import org.testcontainers.containers.output.Slf4jLogConsumer; + import org.testcontainers.containers.wait.strategy.Wait; + import org.testcontainers.images.builder.ImageFromDockerfile; + import org.testcontainers.utility.MountableFile; + + public class SkPubKeyAuthTest extends BaseTestSupport { + + private static final Logger LOG = LoggerFactory.getLogger(SkPubKeyAuthTest.class); + - private static final String TEST_RESOURCES = "org/apache/sshd/client/auth/pubkey"; ++ private static final String TEST_RESOURCES = "org/apache/sshd/docker"; + + private boolean isDockerAvailable() { + try { + DockerClientFactory.instance().client(); + return true; + } catch (Throwable e) { + return false; + } + } + + @Test + void pubkeyAuthOpenSsh() throws Exception { + Assumptions.assumeTrue(isDockerAvailable(), "Docker not available"); + KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC"); + generator.initialize(256); + KeyPair ecKeyPair = generator.generateKeyPair(); + ECPublicKey ecPubKey = ValidateUtils.checkInstanceOf(ecKeyPair.getPublic(), ECPublicKey.class, + "Expected an ECPublicKey"); + SkEcdsaPublicKey fakeSkKey = new SkEcdsaPublicKey("ssh", false, false, ecPubKey); + KeyPair fakeSkKeyPair = new KeyPair(fakeSkKey, ecKeyPair.getPrivate()); + StringBuilder sb = new StringBuilder("verify-required "); + PublicKeyEntry.appendPublicKeyEntry(sb, fakeSkKey); + Path authorizedKeyFile = Files.createTempFile("x", ".x"); + + GenericContainer<?> sshdContainer = null; + try { + Files.write(authorizedKeyFile, Collections.singleton(sb.toString())); + + sshdContainer = new GenericContainer<>(new ImageFromDockerfile() + .withDockerfileFromBuilder(builder -> builder.from("alpine:3.24") // + .run("apk --update add openssh-server") // + .run("ssh-keygen -A") // Generate multiple host keys + .run("adduser -D bob") // Add a user + .run("echo 'bob:passwordBob' | chpasswd") // Give it a password to unlock the user + .run("mkdir -p /home/bob/.ssh") // Create the SSH config directory + .entryPoint("/entrypoint.sh") // Sets bob as owner of anything under /home/bob and launches sshd + .build())) // + .withCopyFileToContainer(MountableFile.forHostPath(authorizedKeyFile), + "/home/bob/.ssh/authorized_keys") + // entrypoint must be executable. Spotbugs doesn't like 0777, so use hex + .withCopyFileToContainer( - MountableFile.forClasspathResource(TEST_RESOURCES + "/entrypoint.sh", 0x1ff), ++ MountableFile.forClasspathResource(TEST_RESOURCES + "/sshd_bob.sh", 0x1ff), + "/entrypoint.sh") + .waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*\\n", 1)) + .withExposedPorts(22) // + .withLogConsumer(new Slf4jLogConsumer(LOG)); + sshdContainer.start(); + + SshClient client = setupTestClient(); + client.setKeyIdentityProvider(KeyPairProvider.wrap(Collections.singleton(fakeSkKeyPair))); + client.start(); + FakeSkKeySignatureFactory skSigner = new FakeSkKeySignatureFactory(fakeSkKey); + String skKeyType = skSigner.getName(); + List<NamedFactory<Signature>> signatures = client.getSignatureFactories(); + List<NamedFactory<Signature>> replaced = new ArrayList<>(); + for (NamedFactory<Signature> s : signatures) { + if (s.getName().equals(skKeyType)) { + replaced.add(skSigner); + } else { + replaced.add(s); + } + } + client.setSignatureFactories(replaced); + + Integer actualPort = sshdContainer.getMappedPort(22); + String actualHost = sshdContainer.getHost(); + try (ClientSession session = client.connect("bob", actualHost, actualPort).verify(CONNECT_TIMEOUT).getSession()) { + session.auth().verify(AUTH_TIMEOUT); + assertTrue(session.isAuthenticated()); + } finally { + client.stop(); + } + } finally { + if (sshdContainer != null) { + sshdContainer.stop(); + } + File f = authorizedKeyFile.toFile(); + if (!f.delete()) { + f.deleteOnExit(); + } + } + } + + @Test + void pubkeyAuth() throws Exception { + KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC"); + generator.initialize(256); + KeyPair ecKeyPair = generator.generateKeyPair(); + ECPublicKey ecPubKey = ValidateUtils.checkInstanceOf(ecKeyPair.getPublic(), ECPublicKey.class, + "Expected an ECPublicKey"); + SkEcdsaPublicKey fakeSkKey = new SkEcdsaPublicKey("ssh", false, false, ecPubKey); + KeyPair fakeSkKeyPair = new KeyPair(fakeSkKey, ecKeyPair.getPrivate()); + StringBuilder sb = new StringBuilder("verify-required "); + PublicKeyEntry.appendPublicKeyEntry(sb, fakeSkKey); + AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(sb.toString()); + + SshServer server = CoreTestSupportUtils.setupTestServer(this.getClass()); + server.setPublickeyAuthenticator(PublickeyAuthenticator.fromAuthorizedEntries("test", null, + Collections.singleton(entry), PublicKeyEntryResolver.FAILING)); + server.start(); + + try { + SshClient client = setupTestClient(); + client.setKeyIdentityProvider(KeyPairProvider.wrap(Collections.singleton(fakeSkKeyPair))); + client.start(); + FakeSkKeySignatureFactory skSigner = new FakeSkKeySignatureFactory(fakeSkKey); + String skKeyType = skSigner.getName(); + List<NamedFactory<Signature>> signatures = client.getSignatureFactories(); + List<NamedFactory<Signature>> replaced = new ArrayList<>(); + for (NamedFactory<Signature> s : signatures) { + if (s.getName().equals(skKeyType)) { + replaced.add(skSigner); + } else { + replaced.add(s); + } + } + client.setSignatureFactories(replaced); + + try (ClientSession session = client.connect("bob", TEST_LOCALHOST, server.getPort()).verify(CONNECT_TIMEOUT) + .getSession()) { + session.auth().verify(AUTH_TIMEOUT); + assertTrue(session.isAuthenticated()); + } finally { + client.stop(); + } + } finally { + server.stop(); + } + } + + private static class FakeSkKeySignatureFactory implements NamedFactory<Signature> { + + private final SkEcdsaPublicKey pub; + + FakeSkKeySignatureFactory(SkEcdsaPublicKey pub) { + this.pub = pub; + } + + @Override + public Signature create() { + return new SignatureSkECDSA() { + + private PrivateKey priv; + + @Override + public void initSigner(SessionContext session, PrivateKey key) { + this.priv = key; + try { + this.challengeDigest = SecurityUtils.getMessageDigest("SHA-256"); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] sign(SessionContext session) { + try { + byte[] sigBlobHash = challengeDigest.digest(); + + byte[] appHash = challengeDigest.digest(pub.getAppName().getBytes(StandardCharsets.UTF_8)); + + Signature signer = BuiltinSignatures.nistp256.create(); + signer.initSigner(null, priv); + signer.update(null, appHash); + byte[] uint = new byte[4]; + uint[0] = 5; // touch & verified + signer.update(null, uint, 0, 1); + BufferUtils.putUInt(42, uint); // Counter + signer.update(null, uint); + signer.update(null, sigBlobHash); + byte[] rawSignature = signer.sign(null); + + ByteArrayBuffer skSignature = new ByteArrayBuffer(); + skSignature.putBytes(rawSignature); + skSignature.putByte((byte) 5); + skSignature.putUInt(42); // Counter + return skSignature.getCompactData(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + } + + @Override + public String getName() { + return pub.getKeyType(); + } + + } + }
