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();
+         }
+ 
+     }
+ }

Reply via email to