This is an automated email from the ASF dual-hosted git repository. twolf pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit bca964c7265c27e415d949e6a7764899012647f4 Author: Thomas Wolf <[email protected]> AuthorDate: Mon Jul 14 21:32:17 2025 +0200 GH-767: Make SkED25519PublicKey work with arbitrary ed25519 keys Remove the hard dependency on net.i2p. --- CHANGES.md | 6 +- .../keys/impl/SkED25519PublicKeyEntryDecoder.java | 20 +++-- .../common/config/keys/u2f/SkED25519PublicKey.java | 17 +++-- .../keys/SkED25519BufferPublicKeyParser.java | 3 +- .../util/security/eddsa/generic/EdDSAUtils.java | 89 ++++++++++++++++++++++ 5 files changed, 118 insertions(+), 17 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a99f71630..99efaf1e0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -39,6 +39,8 @@ * [GH-727](https://github.com/apache/mina-sshd/issues/727) Supply default port 22 for proxy jump hosts for which there is no `HostConfigEntry` * [GH-733](https://github.com/apache/mina-sshd/issues/733) Fix `SftpRemotePathChannel.transferTo()` (avoid NPE) * [GH-751](https://github.com/apache/mina-sshd/issues/751) Fix SFTP v3 "long name" if SFTP server uses an `SftpFileSystem` to another server +* [GH-767](https://github.com/apache/mina-sshd/issues/767) Remove dependency on net.i2p.crypto in `SkED25519PublicKey` +* [GH-771](https://github.com/apache/mina-sshd/issues/771) Remove dependency on net.i2p.crypto in `EdDSAPuttyKeyDecoder` * [GH-774](https://github.com/apache/mina-sshd/issues/774) Fix `WritePendingException` in SFTP file copy @@ -55,8 +57,8 @@ ## Potential Compatibility Issues -Client-side KEX: we've changed the default of the setting `CoreModuleProperties.ABORT_ON_INVALID_CERTIFICATE` from `false` to `true`. -A client will newly abort an SSH connection if the server presents an invalid OpenSSH host certificate as host key. +* Client-side KEX: we've changed the default of the setting `CoreModuleProperties.ABORT_ON_INVALID_CERTIFICATE` from `false` to `true`. A client will newly abort an SSH connection if the server presents an invalid OpenSSH host certificate as host key. +* [GH-767](https://github.com/apache/mina-sshd/issues/767) and [GH-771](https://github.com/apache/mina-sshd/issues/771) cause API changes in classes `SkED25519PublicKey` and `EdDSAPuttyKeyDecoder`. Both changes are unlikely to be noticed in user code since user code normally doesn't need to use either class. ## Major Code Re-factoring diff --git 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 index 550e1b925..1380a2b41 100644 --- 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 @@ -23,21 +23,23 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; +import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Collections; import java.util.Map; import java.util.Objects; -import net.i2p.crypto.eddsa.EdDSAPublicKey; import org.apache.sshd.common.config.keys.KeyEntryResolver; import org.apache.sshd.common.config.keys.u2f.SkED25519PublicKey; import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.common.session.SessionContext; -import org.apache.sshd.common.util.security.eddsa.Ed25519PublicKeyDecoder; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.common.util.security.eddsa.generic.EdDSAUtils; /** * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> @@ -63,10 +65,10 @@ public class SkED25519PublicKeyEntryDecoder extends AbstractPublicKeyEntryDecode } boolean noTouchRequired = parseBooleanHeader(headers, NO_TOUCH_REQUIRED_HEADER, false); - EdDSAPublicKey edDSAPublicKey - = Ed25519PublicKeyDecoder.INSTANCE.decodePublicKey(session, KeyPairProvider.SSH_ED25519, keyData, headers); + 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, edDSAPublicKey); + return new SkED25519PublicKey(appName, noTouchRequired, pk); } @Override @@ -82,8 +84,12 @@ public class SkED25519PublicKeyEntryDecoder extends AbstractPublicKeyEntryDecode public String encodePublicKey(OutputStream s, SkED25519PublicKey key) throws IOException { Objects.requireNonNull(key, "No public key provided"); KeyEntryResolver.encodeString(s, KEY_TYPE); - byte[] seed = Ed25519PublicKeyDecoder.getSeedValue(key.getDelegatePublicKey()); - KeyEntryResolver.writeRLEBytes(s, seed); + try { + byte[] keyData = EdDSAUtils.getBytes(key.getDelegatePublicKey()); + KeyEntryResolver.writeRLEBytes(s, keyData); + } catch (InvalidKeyException e) { + throw new IOException(e.getMessage(), e); + } KeyEntryResolver.encodeString(s, key.getAppName()); return KEY_TYPE; } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/u2f/SkED25519PublicKey.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/u2f/SkED25519PublicKey.java index ebac6a583..a6f525e66 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/u2f/SkED25519PublicKey.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/u2f/SkED25519PublicKey.java @@ -18,24 +18,29 @@ */ package org.apache.sshd.common.config.keys.u2f; +import java.security.PublicKey; import java.util.Objects; -import net.i2p.crypto.eddsa.EdDSAPublicKey; +import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.config.keys.impl.SkED25519PublicKeyEntryDecoder; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.ValidateUtils; -public class SkED25519PublicKey implements SecurityKeyPublicKey<EdDSAPublicKey> { +public class SkED25519PublicKey implements SecurityKeyPublicKey<PublicKey> { public static final String ALGORITHM = "ED25519-SK"; - private static final long serialVersionUID = 4587115316266869640L; + private static final long serialVersionUID = -3947776805731312115L; private final String appName; private final boolean noTouchRequired; - private final EdDSAPublicKey delegatePublicKey; + private final PublicKey delegatePublicKey; - public SkED25519PublicKey(String appName, boolean noTouchRequired, EdDSAPublicKey delegatePublicKey) { + public SkED25519PublicKey(String appName, boolean noTouchRequired, PublicKey delegatePublicKey) { this.appName = appName; this.noTouchRequired = noTouchRequired; + ValidateUtils.checkTrue(KeyPairProvider.SSH_ED25519.equals(KeyUtils.getKeyType(delegatePublicKey)), + "Key is not an ed25519 key"); this.delegatePublicKey = delegatePublicKey; } @@ -70,7 +75,7 @@ public class SkED25519PublicKey implements SecurityKeyPublicKey<EdDSAPublicKey> } @Override - public EdDSAPublicKey getDelegatePublicKey() { + public PublicKey getDelegatePublicKey() { return delegatePublicKey; } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/SkED25519BufferPublicKeyParser.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/SkED25519BufferPublicKeyParser.java index c905e7727..217258de8 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/SkED25519BufferPublicKeyParser.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/keys/SkED25519BufferPublicKeyParser.java @@ -22,7 +22,6 @@ package org.apache.sshd.common.util.buffer.keys; import java.security.GeneralSecurityException; import java.security.PublicKey; -import net.i2p.crypto.eddsa.EdDSAPublicKey; import org.apache.sshd.common.config.keys.impl.SkED25519PublicKeyEntryDecoder; import org.apache.sshd.common.config.keys.u2f.SkED25519PublicKey; import org.apache.sshd.common.keyprovider.KeyPairProvider; @@ -44,6 +43,6 @@ public class SkED25519BufferPublicKeyParser extends AbstractBufferPublicKeyParse // the end PublicKey publicKey = ED25519BufferPublicKeyParser.INSTANCE.getRawPublicKey(KeyPairProvider.SSH_ED25519, buffer); String appName = buffer.getString(); - return new SkED25519PublicKey(appName, false, (EdDSAPublicKey) publicKey); + return new SkED25519PublicKey(appName, false, publicKey); } } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/generic/EdDSAUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/generic/EdDSAUtils.java new file mode 100644 index 000000000..713b49e37 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/generic/EdDSAUtils.java @@ -0,0 +1,89 @@ +/* + * 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.common.util.security.eddsa.generic; + +import java.security.InvalidKeyException; +import java.security.PublicKey; +import java.util.Arrays; + +/** + * Utilities to extract the raw key bytes from ed25519 or ed448 public keys, in a manner that is independent of the + * actual concrete key implementation classes. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public final class EdDSAUtils { + + private static final int ED25519_LENGTH = 32; // bytes + + private static final int ED448_LENGTH = 57; // bytes + + // These are the constant prefixes of X.509 encodings of ed25519 and ed448 keys. Appending the actual 32 + // or 57 key bytes yields valid encodings. + + // Sequence, length 42, Sequence, length 5, OID, length 3, O, I, D, Bit String, length 33, zero unused bits + private static final byte[] ED25519_X509_PREFIX = { + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00 }; + // Sequence, length 67, Sequence, length 5, OID, length 3, O, I, D, Bit String, length 58, zero unused bits + private static final byte[] ED448_X509_PREFIX = { + 0x30, 0x43, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x71, 0x03, 0x3a, 0x00 }; + + private EdDSAUtils() { + throw new IllegalStateException("No instantiation"); + } + + private static boolean startsWith(byte[] data, byte[] prefix) { + if (data == null || prefix == null || prefix.length == 0 || data.length < prefix.length) { + return false; + } + int unequal = 0; + int length = prefix.length; + for (int i = 0; i < length; i++) { + unequal |= data[i] ^ prefix[i]; + } + return unequal == 0; + } + + /** + * Retrieves the raw key bytes from an ed25519 or ed448 {@link PublicKey}. + * + * @param key {@link PublicKey} to get the bytes of + * @return the raw key bytes + * @throws InvalidKeyException if the key is not an ed25519 or ed448 key, or if it doesn't use X.509 encoding + */ + public static byte[] getBytes(PublicKey key) throws InvalidKeyException { + // Extract the public key bytes from the X.509 encoding (last n bytes, depending on the OID). + if (!"X.509".equalsIgnoreCase(key.getFormat())) { + throw new InvalidKeyException("Cannot extract public key bytes from a non-X.509 encoding"); + } + byte[] encoded = key.getEncoded(); + if (encoded == null) { + throw new InvalidKeyException("Public key " + key.getClass().getCanonicalName() + " does not support encoding"); + } + int n; + if (encoded.length == ED25519_LENGTH + ED25519_X509_PREFIX.length && startsWith(encoded, ED25519_X509_PREFIX)) { + n = ED25519_LENGTH; + } else if (encoded.length == ED448_LENGTH + ED448_X509_PREFIX.length && startsWith(encoded, ED448_X509_PREFIX)) { + n = ED448_LENGTH; + } else { + throw new InvalidKeyException("Public key is neither ed25519 nor ed448"); + } + return Arrays.copyOfRange(encoded, encoded.length - n, encoded.length); + } +}
