This is an automated email from the ASF dual-hosted git repository. twolf pushed a commit to branch dev_3.0 in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 9ef3d786a85bc21e51c4f5dfb41f8285f6289dc9 Merge: d087c406c d63179ebe Author: Thomas Wolf <tw...@apache.org> AuthorDate: Wed Aug 27 21:02:44 2025 +0200 Merge branch 'master' into 3.0.0 .../apache/sshd/common/config/keys/KeyUtils.java | 2 + .../keys/impl/SkECDSAPublicKeyEntryDecoder.java | 4 +- .../keys/impl/SkED25519PublicKeyEntryDecoder.java | 4 +- .../config/keys/u2f/SecurityKeyPublicKey.java | 3 + .../common/config/keys/u2f/SkED25519PublicKey.java | 21 ++ .../common/config/keys/u2f/SkEcdsaPublicKey.java | 21 ++ .../signature/AbstractSecurityKeySignature.java | 17 +- .../buffer/keys/SkECBufferPublicKeyParser.java | 2 +- .../keys/SkED25519BufferPublicKeyParser.java | 2 +- ...AuthorizedKeyEntriesPublickeyAuthenticator.java | 3 +- .../sshd/server/auth/pubkey/UserAuthPublicKey.java | 22 ++- ...orizedKeyEntriesPublickeyAuthenticatorTest.java | 71 +++++++ .../sshd/server/auth/UserAuthPublicKeySkTest.java | 214 +++++++++++++++++++++ 13 files changed, 371 insertions(+), 15 deletions(-) diff --cc sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java index d3ed05258,8a4dac941..4e69c6565 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java @@@ -1036,10 -1258,12 +1036,11 @@@ public final class KeyUtils return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals - } else { - return Objects.equals(k1.getAppName(), k2.getAppName()) - && Objects.equals(k1.isNoTouchRequired(), k2.isNoTouchRequired()) - && Objects.equals(k1.isVerifyRequired(), k2.isVerifyRequired()) - && compareECKeys(k1.getDelegatePublicKey(), k2.getDelegatePublicKey()); } + return Objects.equals(k1.getAppName(), k2.getAppName()) + && Objects.equals(k1.isNoTouchRequired(), k2.isNoTouchRequired()) ++ && Objects.equals(k1.isVerifyRequired(), k2.isVerifyRequired()) + && compareECKeys(k1.getDelegatePublicKey(), k2.getDelegatePublicKey()); } public static boolean compareSkEd25519Keys(SkED25519PublicKey k1, SkED25519PublicKey k2) { @@@ -1047,15 -1271,23 +1048,16 @@@ return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals - } else { - return Objects.equals(k1.getAppName(), k2.getAppName()) - && Objects.equals(k1.isNoTouchRequired(), k2.isNoTouchRequired()) - && Objects.equals(k1.isVerifyRequired(), k2.isVerifyRequired()) - && SecurityUtils.compareEDDSAPPublicKeys(k1.getDelegatePublicKey(), k2.getDelegatePublicKey()); } + return Objects.equals(k1.getAppName(), k2.getAppName()) + && Objects.equals(k1.isNoTouchRequired(), k2.isNoTouchRequired()) ++ && Objects.equals(k1.isVerifyRequired(), k2.isVerifyRequired()) + && SecurityUtils.compareEDDSAPPublicKeys(k1.getDelegatePublicKey(), k2.getDelegatePublicKey()); } - public static String getSignatureAlgorithm(String chosenAlgorithm, PublicKey key) { - // check key as we know only certificates require a mapped signature algorithm currently - if (key instanceof OpenSshCertificate) { - synchronized (SIGNATURE_ALGORITHM_MAP) { - return SIGNATURE_ALGORITHM_MAP.get(chosenAlgorithm); - } - } else { - return chosenAlgorithm; - } + public static String getSignatureAlgorithm(String chosenAlgorithm) { + String mapped = SIGNATURE_ALGORITHM_MAP.get(chosenAlgorithm); + return mapped == null ? chosenAlgorithm : mapped; } public static boolean isCertificateAlgorithm(String algorithm) { diff --cc sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/SkECDSAPublicKeyEntryDecoder.java index 0ee5179e1,a6a51be77..5e2dbda57 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/SkECDSAPublicKeyEntryDecoder.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/SkECDSAPublicKeyEntryDecoder.java @@@ -62,11 -63,23 +63,12 @@@ public class SkECDSAPublicKeyEntryDecod } boolean noTouchRequired = parseBooleanHeader(headers, NO_TOUCH_REQUIRED_HEADER, false); + boolean verifyRequired = parseBooleanHeader(headers, VERIFY_REQUIRED_HEADER, false); ECPublicKey ecPublicKey = ECDSAPublicKeyEntryDecoder.INSTANCE.decodePublicKey(ECCurves.nistp256, keyData); String appName = KeyEntryResolver.decodeString(keyData, MAX_APP_NAME_LENGTH); - return new SkEcdsaPublicKey(appName, noTouchRequired, ecPublicKey); + return new SkEcdsaPublicKey(appName, noTouchRequired, verifyRequired, ecPublicKey); } - @Override - public SkEcdsaPublicKey clonePublicKey(SkEcdsaPublicKey key) throws GeneralSecurityException { - if (key == null) { - return null; - } - - return new SkEcdsaPublicKey( - key.getAppName(), key.isNoTouchRequired(), key.isVerifyRequired(), - ECDSAPublicKeyEntryDecoder.INSTANCE.clonePublicKey(key.getDelegatePublicKey())); - } - @Override public String encodePublicKey(OutputStream s, SkEcdsaPublicKey key) throws IOException { Objects.requireNonNull(key, "No public key provided"); diff --cc sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/SkED25519PublicKeyEntryDecoder.java index d41bcce7c,4a39e2d05..6fa4e22f0 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/SkED25519PublicKeyEntryDecoder.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/SkED25519PublicKeyEntryDecoder.java @@@ -68,9 -70,19 +70,9 @@@ public class SkED25519PublicKeyEntryDec PublicKey pk = SecurityUtils.getEDDSAPublicKeyEntryDecoder().decodePublicKey(session, KeyPairProvider.SSH_ED25519, keyData, headers); String appName = KeyEntryResolver.decodeString(keyData, MAX_APP_NAME_LENGTH); - return new SkED25519PublicKey(appName, noTouchRequired, pk); + return new SkED25519PublicKey(appName, noTouchRequired, verifyRequired, pk); } - @Override - public SkED25519PublicKey clonePublicKey(SkED25519PublicKey key) { - if (key == null) { - return null; - } - - return new SkED25519PublicKey(key.getAppName(), key.isNoTouchRequired(), key.isVerifyRequired(), - key.getDelegatePublicKey()); - } - @Override public String encodePublicKey(OutputStream s, SkED25519PublicKey key) throws IOException { Objects.requireNonNull(key, "No public key provided"); diff --cc sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/UserAuthPublicKey.java index 2195f2de9,a1f07629d..a732bb382 --- 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 @@@ -182,9 -196,24 +196,9 @@@ public class UserAuthPublicKey extends } protected void verifyCertificateSignature(ServerSession session, OpenSshCertificate cert) throws Exception { - if (!OpenSshCertificate.verifySignature(cert, session.getSignatureFactories())) { - PublicKey signatureKey = cert.getCaPubKey(); - String keyAlg = KeyUtils.getKeyType(signatureKey); - String keyId = cert.getId(); - - String sigAlg = cert.getSignatureAlgorithm(); - if (!keyAlg.equals(KeyUtils.getCanonicalKeyType(sigAlg))) { ++ if (!OpenSshCertificate.verifySignature(cert, SignatureFactoriesManager.resolveSignatureFactories(this, session))) { throw new CertificateException( - "Found invalid signature alg " + sigAlg + " for key ID=" + keyId + " using a " + keyAlg + " CA key"); - } - - Signature verif = ValidateUtils.checkNotNull( - NamedFactory.create(SignatureFactoriesManager.resolveSignatureFactories(this, session), sigAlg), - "No CA verifier located for algorithm=%s of key ID=%s", sigAlg, keyId); - verif.initVerifier(session, signatureKey); - verif.update(session, cert.getMessage()); - - if (!verif.verify(session, cert.getSignature())) { - throw new CertificateException("CA signature verification failed for key type=" + keyAlg + " of key ID=" + keyId); + "CA signature verification failed for key type=" + cert.getKeyType() + " of key ID=" + cert.getId()); } } diff --cc sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeySkTest.java index 000000000,cba684dc4..0543f4140 mode 000000,100644..100644 --- a/sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeySkTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/UserAuthPublicKeySkTest.java @@@ -1,0 -1,207 +1,214 @@@ + /* + * 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.nio.charset.StandardCharsets; + import java.security.KeyPair; + import java.security.KeyPairGenerator; + import java.security.MessageDigest; + import java.security.PrivateKey; + import java.security.SignatureException; + import java.security.interfaces.ECPublicKey; + import java.util.Collections; + import java.util.concurrent.ThreadLocalRandom; + + 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.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.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.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.ServerFactoryManager; + import org.apache.sshd.server.auth.pubkey.AuthorizedKeyEntriesPublickeyAuthenticator; + 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 sk-* authentication on the server-side. + */ + @Tag("NoIoTestCase") + class UserAuthPublicKeySkTest extends BaseTestSupport { + + @ParameterizedTest(name = "auth {0} sig {1}") + @CsvSource({ // + "0,0", "0,1", "0,4", "0,5", // + "1,0", "1,1", "1,4", "1,5", // + "4,0", "4,1", "4,4", "4,5", // + "5,0", "5,1", "5,4", "5,5" // + }) + void testSk(int authFlags, int flagsOnSig) throws Exception { + // Determine expected outcome + boolean expectSuccess = true; + if ((flagsOnSig & 1) == 0 && (authFlags & 1) == 0) { + // Incoming key/signature doesn't have "user presence" flag but auth requires is (no-touch-required not set) + expectSuccess = false; + } + if ((authFlags & 4) != 0 && (flagsOnSig & 4) == 0) { + // auth has verify-required but incoming key/signature doesn't. + expectSuccess = false; + } + + // Generate a "fake" SK EC key. "Fake" because we actually use a normal EC key as basis, so that we have the + // private key + // and can generate a signature. + KeyPairGenerator generator = SecurityUtils.getKeyPairGenerator("EC"); + generator.initialize(256); + KeyPair pair = generator.generateKeyPair(); + ECPublicKey ecPubKey = ValidateUtils.checkInstanceOf(pair.getPublic(), ECPublicKey.class, "Expected an ECPublicKey"); + SkEcdsaPublicKey sk = new SkEcdsaPublicKey("ssh", false, false, ecPubKey); + + 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); + + // Generate an AuthorizedKeyEntry, and set an authenticator on the mock session. + String entryLine = ""; + switch (authFlags & 5) { + case 0: + break; + case 1: + entryLine = "no-touch-required "; + break; + case 4: + entryLine = "verify-required "; + break; + case 5: + entryLine = "no-touch-required,verify-required "; + break; + default: + fail("Invalid authFlags " + authFlags); + break; + } + entryLine += PublicKeyEntry.toString(sk); + AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(entryLine); + AuthorizedKeyEntriesPublickeyAuthenticator authenticator = new AuthorizedKeyEntriesPublickeyAuthenticator("test", + session, Collections.singleton(entry), PublicKeyEntryResolver.FAILING); + session.setPublickeyAuthenticator(authenticator); + + // Create the UserAuthPublickey object under test; give it the signature factory we need. + UserAuthPublicKey pubkeyAuth = new UserAuthPublicKey( + Collections.singletonList(BuiltinSignatures.sk_ecdsa_sha2_nistp256)); + + // Create a buffer with a full properly signed authentication request. + ByteArrayBuffer buffer = createRequest(id, (byte) flagsOnSig, sk, 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 simple 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); + return new MockSession(manager, Mockito.mock(IoSession.class)); + } + + private ByteArrayBuffer createRequest(byte[] sessionId, byte flagsOnSig, SkEcdsaPublicKey sk, 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(sk.getKeyType()); // Algorithm + payload.putPublicKey(sk); + + MessageDigest md = SecurityUtils.getMessageDigest("SHA-256"); + byte[] uint = new byte[4]; + BufferUtils.putUInt(sessionId.length, uint); + md.update(uint); + md.update(sessionId); + md.update(SshConstants.SSH_MSG_USERAUTH_REQUEST); + byte[] sigBlobHash = md.digest(payload.getCompactData()); + + byte[] appHash = md.digest(sk.getAppName().getBytes(StandardCharsets.UTF_8)); + + Signature signer = BuiltinSignatures.nistp256.create(); + signer.initSigner(null, priv); + signer.update(null, appHash); + uint[0] = flagsOnSig; + signer.update(null, uint, 0, 1); + BufferUtils.putUInt(42, uint); // Counter + signer.update(null, uint); + // Extensions only for webauthn, in which case they would also be in the skSignature below. + signer.update(null, sigBlobHash); + byte[] rawSignature = signer.sign(null); + + ByteArrayBuffer skSignature = new ByteArrayBuffer(); + skSignature.putString(sk.getKeyType()); + skSignature.putBytes(rawSignature); + skSignature.putByte(flagsOnSig); + skSignature.putUInt(42); // Counter + // Webauthn stuff would follow. + + payload.putBytes(skSignature.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; ++ } + } + }