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

Reply via email to