This is an automated email from the ASF dual-hosted git repository. tomaswolf pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 206f54b3702d91e98cd251a2f8ed02508962e48a Author: Thomas Wolf <[email protected]> AuthorDate: Sun Jun 21 14:52:19 2026 +0200 Integration test for SK keys Test the client side connecting with [email protected] to OpenSSH. Create an ECDSA key pair, and manually create the SK signature using the private key. A second test then tests the server side in Apache MINA SSHD by connecting in the same way to an Apache MINA SSHD server. --- CHANGES.md | 1 + .../signature/AbstractSecurityKeySignature.java | 3 +- .../sshd/client/auth/pubkey/SkPubKeyAuthTest.java | 261 +++++++++++++++++++++ sshd-mina/pom.xml | 2 + sshd-netty/pom.xml | 1 + 5 files changed, 267 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index cfdde8b93..880c79361 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -32,6 +32,7 @@ ## Bug Fixes +* [GH-902](https://github.com/apache/mina-sshd/pull/902) Fix client-side handling of sk-* public key signatures (also in the agent interfaces) ## New Features diff --git a/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSecurityKeySignature.java b/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSecurityKeySignature.java index b1b66b2f5..c293d16cf 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSecurityKeySignature.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSecurityKeySignature.java @@ -36,9 +36,10 @@ public abstract class AbstractSecurityKeySignature implements Signature { private static final int FLAG_VERIFIED = 1 << 2; + protected MessageDigest challengeDigest; + private final String keyType; private SecurityKeyPublicKey<?> publicKey; - private MessageDigest challengeDigest; protected AbstractSecurityKeySignature(String keyType) { this.keyType = keyType; diff --git a/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/SkPubKeyAuthTest.java b/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/SkPubKeyAuthTest.java new file mode 100644 index 000000000..68c120406 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/client/auth/pubkey/SkPubKeyAuthTest.java @@ -0,0 +1,261 @@ +/* + * 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; + +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.forward.PortForwardingWithOpenSshTest; +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 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), + "/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(PortForwardingWithOpenSshTest.class); + 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(); + } + + } +} diff --git a/sshd-mina/pom.xml b/sshd-mina/pom.xml index cb77c20c5..765f9f84f 100644 --- a/sshd-mina/pom.xml +++ b/sshd-mina/pom.xml @@ -127,6 +127,7 @@ <exclude>**/OpenSshCipherTest.java</exclude> <exclude>**/OpenSshMlKemTest.java</exclude> <exclude>**/PortForwardingWithOpenSshTest.java</exclude> + <exclude>**/SkPubKeyAuthTest.java</exclude> <exclude>**/StrictKexInteroperabilityTest.java</exclude> <!-- reading files from classpath doesn't work correctly w/ reusable test jar --> <exclude>**/OpenSSHCertificateTest.java</exclude> @@ -180,6 +181,7 @@ <exclude>**/OpenSshCipherTest.java</exclude> <exclude>**/OpenSshMlKemTest.java</exclude> <exclude>**/PortForwardingWithOpenSshTest.java</exclude> + <exclude>**/SkPubKeyAuthTest.java</exclude> <exclude>**/StrictKexInteroperabilityTest.java</exclude> <!-- reading files from classpath doesn't work correctly w/ reusable test jar --> <exclude>**/OpenSSHCertificateTest.java</exclude> diff --git a/sshd-netty/pom.xml b/sshd-netty/pom.xml index 975f6b6d0..253fa8313 100644 --- a/sshd-netty/pom.xml +++ b/sshd-netty/pom.xml @@ -151,6 +151,7 @@ <exclude>**/OpenSshCipherTest.java</exclude> <exclude>**/OpenSshMlKemTest.java</exclude> <exclude>**/PortForwardingWithOpenSshTest.java</exclude> + <exclude>**/SkPubKeyAuthTest.java</exclude> <exclude>**/StrictKexInteroperabilityTest.java</exclude> <!-- reading files from classpath doesn't work correctly w/ reusable test jar --> <exclude>**/OpenSSHCertificateTest.java</exclude>
