Skip to content

Commit 206f54b

Browse files
committed
Integration test for SK keys
Test the client side connecting with sk-ecdsa-sha2-nistp256@openssh.com 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.
1 parent f463789 commit 206f54b

5 files changed

Lines changed: 267 additions & 1 deletion

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
## Bug Fixes
3434

35+
* [GH-902](https://github.com/apache/mina-sshd/pull/902) Fix client-side handling of sk-* public key signatures (also in the agent interfaces)
3536

3637
## New Features
3738

sshd-common/src/main/java/org/apache/sshd/common/signature/AbstractSecurityKeySignature.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ public abstract class AbstractSecurityKeySignature implements Signature {
3636

3737
private static final int FLAG_VERIFIED = 1 << 2;
3838

39+
protected MessageDigest challengeDigest;
40+
3941
private final String keyType;
4042
private SecurityKeyPublicKey<?> publicKey;
41-
private MessageDigest challengeDigest;
4243

4344
protected AbstractSecurityKeySignature(String keyType) {
4445
this.keyType = keyType;
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.sshd.client.auth.pubkey;
20+
21+
import java.io.File;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.security.GeneralSecurityException;
26+
import java.security.KeyPair;
27+
import java.security.KeyPairGenerator;
28+
import java.security.PrivateKey;
29+
import java.security.interfaces.ECPublicKey;
30+
import java.util.ArrayList;
31+
import java.util.Collections;
32+
import java.util.List;
33+
34+
import org.apache.sshd.client.SshClient;
35+
import org.apache.sshd.client.session.ClientSession;
36+
import org.apache.sshd.common.NamedFactory;
37+
import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
38+
import org.apache.sshd.common.config.keys.PublicKeyEntry;
39+
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
40+
import org.apache.sshd.common.config.keys.u2f.SkEcdsaPublicKey;
41+
import org.apache.sshd.common.forward.PortForwardingWithOpenSshTest;
42+
import org.apache.sshd.common.keyprovider.KeyPairProvider;
43+
import org.apache.sshd.common.session.SessionContext;
44+
import org.apache.sshd.common.signature.BuiltinSignatures;
45+
import org.apache.sshd.common.signature.Signature;
46+
import org.apache.sshd.common.signature.SignatureSkECDSA;
47+
import org.apache.sshd.common.util.ValidateUtils;
48+
import org.apache.sshd.common.util.buffer.BufferUtils;
49+
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
50+
import org.apache.sshd.common.util.security.SecurityUtils;
51+
import org.apache.sshd.server.SshServer;
52+
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
53+
import org.apache.sshd.util.test.BaseTestSupport;
54+
import org.apache.sshd.util.test.CoreTestSupportUtils;
55+
import org.junit.jupiter.api.Assumptions;
56+
import org.junit.jupiter.api.Test;
57+
import org.slf4j.Logger;
58+
import org.slf4j.LoggerFactory;
59+
import org.testcontainers.DockerClientFactory;
60+
import org.testcontainers.containers.GenericContainer;
61+
import org.testcontainers.containers.output.Slf4jLogConsumer;
62+
import org.testcontainers.containers.wait.strategy.Wait;
63+
import org.testcontainers.images.builder.ImageFromDockerfile;
64+
import org.testcontainers.utility.MountableFile;
65+
66+
public class SkPubKeyAuthTest extends BaseTestSupport {
67+
68+
private static final Logger LOG = LoggerFactory.getLogger(SkPubKeyAuthTest.class);
69+
70+
private static final String TEST_RESOURCES = "org/apache/sshd/client/auth/pubkey";
71+
72+
private boolean isDockerAvailable() {
73+
try {
74+
DockerClientFactory.instance().client();
75+
return true;
76+
} catch (Throwable e) {
77+
return false;
78+
}
79+
}
80+
81+
@Test
82+
void pubkeyAuthOpenSsh() throws Exception {
83+
Assumptions.assumeTrue(isDockerAvailable(), "Docker not available");
84+
KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC");
85+
generator.initialize(256);
86+
KeyPair ecKeyPair = generator.generateKeyPair();
87+
ECPublicKey ecPubKey = ValidateUtils.checkInstanceOf(ecKeyPair.getPublic(), ECPublicKey.class,
88+
"Expected an ECPublicKey");
89+
SkEcdsaPublicKey fakeSkKey = new SkEcdsaPublicKey("ssh", false, false, ecPubKey);
90+
KeyPair fakeSkKeyPair = new KeyPair(fakeSkKey, ecKeyPair.getPrivate());
91+
StringBuilder sb = new StringBuilder("verify-required ");
92+
PublicKeyEntry.appendPublicKeyEntry(sb, fakeSkKey);
93+
Path authorizedKeyFile = Files.createTempFile("x", ".x");
94+
95+
GenericContainer<?> sshdContainer = null;
96+
try {
97+
Files.write(authorizedKeyFile, Collections.singleton(sb.toString()));
98+
99+
sshdContainer = new GenericContainer<>(new ImageFromDockerfile()
100+
.withDockerfileFromBuilder(builder -> builder.from("alpine:3.24") //
101+
.run("apk --update add openssh-server") //
102+
.run("ssh-keygen -A") // Generate multiple host keys
103+
.run("adduser -D bob") // Add a user
104+
.run("echo 'bob:passwordBob' | chpasswd") // Give it a password to unlock the user
105+
.run("mkdir -p /home/bob/.ssh") // Create the SSH config directory
106+
.entryPoint("/entrypoint.sh") // Sets bob as owner of anything under /home/bob and launches sshd
107+
.build())) //
108+
.withCopyFileToContainer(MountableFile.forHostPath(authorizedKeyFile),
109+
"/home/bob/.ssh/authorized_keys")
110+
// entrypoint must be executable. Spotbugs doesn't like 0777, so use hex
111+
.withCopyFileToContainer(
112+
MountableFile.forClasspathResource(TEST_RESOURCES + "/entrypoint.sh", 0x1ff),
113+
"/entrypoint.sh")
114+
.waitingFor(Wait.forLogMessage(".*Server listening on :: port 22.*\\n", 1))
115+
.withExposedPorts(22) //
116+
.withLogConsumer(new Slf4jLogConsumer(LOG));
117+
sshdContainer.start();
118+
119+
SshClient client = setupTestClient();
120+
client.setKeyIdentityProvider(KeyPairProvider.wrap(Collections.singleton(fakeSkKeyPair)));
121+
client.start();
122+
FakeSkKeySignatureFactory skSigner = new FakeSkKeySignatureFactory(fakeSkKey);
123+
String skKeyType = skSigner.getName();
124+
List<NamedFactory<Signature>> signatures = client.getSignatureFactories();
125+
List<NamedFactory<Signature>> replaced = new ArrayList<>();
126+
for (NamedFactory<Signature> s : signatures) {
127+
if (s.getName().equals(skKeyType)) {
128+
replaced.add(skSigner);
129+
} else {
130+
replaced.add(s);
131+
}
132+
}
133+
client.setSignatureFactories(replaced);
134+
135+
Integer actualPort = sshdContainer.getMappedPort(22);
136+
String actualHost = sshdContainer.getHost();
137+
try (ClientSession session = client.connect("bob", actualHost, actualPort).verify(CONNECT_TIMEOUT).getSession()) {
138+
session.auth().verify(AUTH_TIMEOUT);
139+
assertTrue(session.isAuthenticated());
140+
} finally {
141+
client.stop();
142+
}
143+
} finally {
144+
if (sshdContainer != null) {
145+
sshdContainer.stop();
146+
}
147+
File f = authorizedKeyFile.toFile();
148+
if (!f.delete()) {
149+
f.deleteOnExit();
150+
}
151+
}
152+
}
153+
154+
@Test
155+
void pubkeyAuth() throws Exception {
156+
KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC");
157+
generator.initialize(256);
158+
KeyPair ecKeyPair = generator.generateKeyPair();
159+
ECPublicKey ecPubKey = ValidateUtils.checkInstanceOf(ecKeyPair.getPublic(), ECPublicKey.class,
160+
"Expected an ECPublicKey");
161+
SkEcdsaPublicKey fakeSkKey = new SkEcdsaPublicKey("ssh", false, false, ecPubKey);
162+
KeyPair fakeSkKeyPair = new KeyPair(fakeSkKey, ecKeyPair.getPrivate());
163+
StringBuilder sb = new StringBuilder("verify-required ");
164+
PublicKeyEntry.appendPublicKeyEntry(sb, fakeSkKey);
165+
AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(sb.toString());
166+
167+
SshServer server = CoreTestSupportUtils.setupTestServer(PortForwardingWithOpenSshTest.class);
168+
server.setPublickeyAuthenticator(PublickeyAuthenticator.fromAuthorizedEntries("test", null,
169+
Collections.singleton(entry), PublicKeyEntryResolver.FAILING));
170+
server.start();
171+
172+
try {
173+
SshClient client = setupTestClient();
174+
client.setKeyIdentityProvider(KeyPairProvider.wrap(Collections.singleton(fakeSkKeyPair)));
175+
client.start();
176+
FakeSkKeySignatureFactory skSigner = new FakeSkKeySignatureFactory(fakeSkKey);
177+
String skKeyType = skSigner.getName();
178+
List<NamedFactory<Signature>> signatures = client.getSignatureFactories();
179+
List<NamedFactory<Signature>> replaced = new ArrayList<>();
180+
for (NamedFactory<Signature> s : signatures) {
181+
if (s.getName().equals(skKeyType)) {
182+
replaced.add(skSigner);
183+
} else {
184+
replaced.add(s);
185+
}
186+
}
187+
client.setSignatureFactories(replaced);
188+
189+
try (ClientSession session = client.connect("bob", TEST_LOCALHOST, server.getPort()).verify(CONNECT_TIMEOUT)
190+
.getSession()) {
191+
session.auth().verify(AUTH_TIMEOUT);
192+
assertTrue(session.isAuthenticated());
193+
} finally {
194+
client.stop();
195+
}
196+
} finally {
197+
server.stop();
198+
}
199+
}
200+
201+
private static class FakeSkKeySignatureFactory implements NamedFactory<Signature> {
202+
203+
private final SkEcdsaPublicKey pub;
204+
205+
FakeSkKeySignatureFactory(SkEcdsaPublicKey pub) {
206+
this.pub = pub;
207+
}
208+
209+
@Override
210+
public Signature create() {
211+
return new SignatureSkECDSA() {
212+
213+
private PrivateKey priv;
214+
215+
@Override
216+
public void initSigner(SessionContext session, PrivateKey key) {
217+
this.priv = key;
218+
try {
219+
this.challengeDigest = SecurityUtils.getMessageDigest("SHA-256");
220+
} catch (GeneralSecurityException e) {
221+
throw new RuntimeException(e);
222+
}
223+
}
224+
225+
@Override
226+
public byte[] sign(SessionContext session) {
227+
try {
228+
byte[] sigBlobHash = challengeDigest.digest();
229+
230+
byte[] appHash = challengeDigest.digest(pub.getAppName().getBytes(StandardCharsets.UTF_8));
231+
232+
Signature signer = BuiltinSignatures.nistp256.create();
233+
signer.initSigner(null, priv);
234+
signer.update(null, appHash);
235+
byte[] uint = new byte[4];
236+
uint[0] = 5; // touch & verified
237+
signer.update(null, uint, 0, 1);
238+
BufferUtils.putUInt(42, uint); // Counter
239+
signer.update(null, uint);
240+
signer.update(null, sigBlobHash);
241+
byte[] rawSignature = signer.sign(null);
242+
243+
ByteArrayBuffer skSignature = new ByteArrayBuffer();
244+
skSignature.putBytes(rawSignature);
245+
skSignature.putByte((byte) 5);
246+
skSignature.putUInt(42); // Counter
247+
return skSignature.getCompactData();
248+
} catch (Exception e) {
249+
throw new RuntimeException(e);
250+
}
251+
}
252+
};
253+
}
254+
255+
@Override
256+
public String getName() {
257+
return pub.getKeyType();
258+
}
259+
260+
}
261+
}

sshd-mina/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
<exclude>**/OpenSshCipherTest.java</exclude>
128128
<exclude>**/OpenSshMlKemTest.java</exclude>
129129
<exclude>**/PortForwardingWithOpenSshTest.java</exclude>
130+
<exclude>**/SkPubKeyAuthTest.java</exclude>
130131
<exclude>**/StrictKexInteroperabilityTest.java</exclude>
131132
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
132133
<exclude>**/OpenSSHCertificateTest.java</exclude>
@@ -180,6 +181,7 @@
180181
<exclude>**/OpenSshCipherTest.java</exclude>
181182
<exclude>**/OpenSshMlKemTest.java</exclude>
182183
<exclude>**/PortForwardingWithOpenSshTest.java</exclude>
184+
<exclude>**/SkPubKeyAuthTest.java</exclude>
183185
<exclude>**/StrictKexInteroperabilityTest.java</exclude>
184186
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
185187
<exclude>**/OpenSSHCertificateTest.java</exclude>

sshd-netty/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
<exclude>**/OpenSshCipherTest.java</exclude>
152152
<exclude>**/OpenSshMlKemTest.java</exclude>
153153
<exclude>**/PortForwardingWithOpenSshTest.java</exclude>
154+
<exclude>**/SkPubKeyAuthTest.java</exclude>
154155
<exclude>**/StrictKexInteroperabilityTest.java</exclude>
155156
<!-- reading files from classpath doesn't work correctly w/ reusable test jar -->
156157
<exclude>**/OpenSSHCertificateTest.java</exclude>

0 commit comments

Comments
 (0)