Skip to content

Commit

Permalink
GH-636: Handle unknown key types in known_hosts
Browse files Browse the repository at this point in the history
Harden the parser so that it can parse known_host and authorized_key
lines with unknown key types. Introduce a new UnsupportedSshPublicKey
class to be able to deal with such entries later on when the server
host key is compared. (An alternative would have been not to create
PublicKeys from known_host lines at all but to serialize the given
server key into string form and then just compare against the string
from the known_host line. But that is not possible without breaking
API...)

Such an UnsupportedSshPublicKey supports getting its key type, its raw
key data, its fingerprint, and it can be written into a Buffer.
  • Loading branch information
tomaswolf committed Nov 22, 2024
1 parent 690be26 commit 7cc9c49
Show file tree
Hide file tree
Showing 17 changed files with 393 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
* [GH-618](https://github.com/apache/mina-sshd/issues/618) Fix reading an `OpenSshCertificate` from a `Buffer`
* [GH-626](https://github.com/apache/mina-sshd/issues/626) Enable `Streaming.Async` for `ChannelDirectTcpip`
* [GH-628](https://github.com/apache/mina-sshd/issues/628) SFTP: fix reading directories with trailing blanks in the name
* [GH-636](https://github.com/apache/mina-sshd/issues/636) Fix handling of unsupported key types in `known_hosts` file

## New Features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,8 @@ public static <E extends KnownHostEntry> E parseKnownHostEntry(E entry, String d
entry.setHashedEntry(null);
entry.setPatterns(parsePatterns(GenericUtils.split(hostPattern, ',')));
}

AuthorizedKeyEntry key = ValidateUtils.checkNotNull(AuthorizedKeyEntry.parseAuthorizedKeyEntry(line),
"No valid key entry recovered from line=%s", data);
AuthorizedKeyEntry key = PublicKeyEntry.parsePublicKeyEntry(new AuthorizedKeyEntry(),
ValidateUtils.checkNotNullAndNotEmpty(line, "No valid key entry recovered from line=%s", data));
entry.setKeyEntry(key);
return entry;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,36 @@ public static AuthorizedKeyEntry parseAuthorizedKeyEntry(
decoder = KeyUtils.getPublicKeyEntryDecoder(keyType);
}

AuthorizedKeyEntry entry;
AuthorizedKeyEntry entry = null;
// assume this is due to the fact that it starts with login options
if (decoder == null) {
Map.Entry<String, String> comps = resolveEntryComponents(line);
entry = parseAuthorizedKeyEntry(comps.getValue());
ValidateUtils.checkTrue(entry != null, "Bad format (no key data after login options): %s", line);
entry.setLoginOptions(parseLoginOptions(comps.getKey()));
} else {
String keyData = comps.getValue();
String options = comps.getKey();
if (keyData.startsWith("AAAA")) {
// OpenSSH known_hosts is defined to use base64, and the key data contains the binary encoded string for
// the key type again. So the base64 data always starts off with the uint32 length of the key type, with
// always starts with at least 3 zero bytes (assuming the key type has less than 256 characters). 3 zero
// bytes yield 4 A's in base64.
//
// So here we know that resolveEntryComponents() read ahead one token too far. This may happen if we
// don't support the key type (we have no decoder for it).
int i = options.lastIndexOf(' ');
if (i < 0) {
// options must be equal to keyType. Just handle the original line.
keyData = null;
} else {
keyData = options.substring(i + 1) + ' ' + keyData;
options = options.substring(0, i);
}
}
if (keyData != null) {
entry = parseAuthorizedKeyEntry(keyData, resolver);
ValidateUtils.checkTrue(entry != null, "Bad format (no key data after login options): %s", line);
entry.setLoginOptions(parseLoginOptions(options));
}
}
if (entry == null) {
String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
entry = parsePublicKeyEntry(new AuthorizedKeyEntry(), encData, resolver);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,8 @@ public static String getKeyType(KeyPair kp) {
public static String getKeyType(Key key) {
if (key == null) {
return null;
} else if (key instanceof SshPublicKey) {
return ((SshPublicKey) key).getKeyType();
} else if (key instanceof DSAKey) {
return KeyPairProvider.SSH_DSS;
} else if (key instanceof RSAKey) {
Expand All @@ -872,14 +874,8 @@ public static String getKeyType(Key key) {
} else {
return curve.getKeyType();
}
} else if (key instanceof SkEcdsaPublicKey) {
return SkECDSAPublicKeyEntryDecoder.KEY_TYPE;
} else if (SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) {
return KeyPairProvider.SSH_ED25519;
} else if (key instanceof SkED25519PublicKey) {
return SkED25519PublicKeyEntryDecoder.KEY_TYPE;
} else if (key instanceof OpenSshCertificate) {
return ((OpenSshCertificate) key).getKeyType();
}

return null;
Expand Down Expand Up @@ -1063,6 +1059,9 @@ public static boolean compareKeyPairs(KeyPair k1, KeyPair k2) {
}

public static boolean compareKeys(PublicKey k1, PublicKey k2) {
if (Objects.equals(k1, k2)) {
return true;
}
if ((k1 instanceof RSAPublicKey) && (k2 instanceof RSAPublicKey)) {
return compareRSAKeys(RSAPublicKey.class.cast(k1), RSAPublicKey.class.cast(k2));
} else if ((k1 instanceof DSAPublicKey) && (k2 instanceof DSAPublicKey)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
* @see <a href= "https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD">PROTOCOL.certkeys</a>
*/
public interface OpenSshCertificate extends PublicKey, PrivateKey {
public interface OpenSshCertificate extends SshPublicKey, PrivateKey {

/**
* {@link OpenSshCertificate}s have a type indicating whether the certificate if for a host key (certifying a host
Expand Down Expand Up @@ -90,13 +90,6 @@ public static Type fromCode(int code) {
*/
byte[] getNonce();

/**
* Retrieves the SSH key type of this certificate.
*
* @return the key type, for instance "ssh-rsa-cert-v01@openssh.com"
*/
String getKeyType();

/**
* Retrieves the certified public key.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,24 +482,32 @@ public static <A extends Appendable> A appendPublicKeyEntry(
return sb;
}

@SuppressWarnings("unchecked")
PublicKeyEntryDecoder<PublicKey, ?> decoder
= (PublicKeyEntryDecoder<PublicKey, ?>) KeyUtils.getPublicKeyEntryDecoder(key);
if (decoder == null) {
throw new StreamCorruptedException("Cannot retrieve decoder for key=" + key.getAlgorithm());
}
String keyType;
byte[] bytes;

try (ByteArrayOutputStream s = new ByteArrayOutputStream(Byte.MAX_VALUE)) {
String keyType = decoder.encodePublicKey(s, key);
byte[] bytes = s.toByteArray();
if (encoder == null) {
encoder = resolveKeyDataEntryResolver(keyType);
if (key instanceof UnsupportedSshPublicKey) {
UnsupportedSshPublicKey unsupported = (UnsupportedSshPublicKey) key;
keyType = unsupported.getKeyType();
bytes = unsupported.getKeyData();
} else {
@SuppressWarnings("unchecked")
PublicKeyEntryDecoder<PublicKey, ?> decoder = (PublicKeyEntryDecoder<PublicKey, ?>) KeyUtils
.getPublicKeyEntryDecoder(key);
if (decoder == null) {
throw new StreamCorruptedException("Cannot retrieve decoder for key=" + key.getAlgorithm());
}

String encData = encoder.encodeEntryKeyData(bytes);
sb.append(keyType).append(' ').append(encData);
try (ByteArrayOutputStream s = new ByteArrayOutputStream(Byte.MAX_VALUE)) {
keyType = decoder.encodePublicKey(s, key);
bytes = s.toByteArray();
}
}
if (encoder == null) {
encoder = resolveKeyDataEntryResolver(keyType);
}

String encData = encoder.encodeEntryKeyData(bytes);
sb.append(keyType).append(' ').append(encData);
return sb;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@ public String toString() {
}
};

/**
* A resolver that returns an {@link UnsupportedSshPublicKey} for any input.
*/
PublicKeyEntryResolver UNSUPPORTED = new PublicKeyEntryResolver() {
@Override
public PublicKey resolve(SessionContext session, String keyType, byte[] keyData, Map<String, String> headers)
throws IOException, GeneralSecurityException {
return new UnsupportedSshPublicKey(keyType, keyData);
}

@Override
public String toString() {
return "UNSUPPORTED";
}
};

/**
* @param session The {@link SessionContext} for invoking this load command - may be {@code null}
* if not invoked within a session context (e.g., offline tool or session unknown).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.config.keys;

import java.security.PublicKey;

/**
* A {@link PublicKey} that has an SSH key type.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public interface SshPublicKey extends PublicKey {

/**
* Retrieves the SSH key type.
*
* @return the SSH key type, never {@code null}.
*/
String getKeyType();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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.config.keys;

import java.util.Arrays;
import java.util.Objects;

/**
* A representation of an unsupported SSH public key -- just a key type and the raw key data.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class UnsupportedSshPublicKey implements SshPublicKey {

private static final long serialVersionUID = -4870624671501562706L;

private final String keyType;

private final byte[] keyData;

public UnsupportedSshPublicKey(String keyType, byte[] keyData) {
this.keyType = keyType;
this.keyData = keyData.clone();
}

@Override
public String getAlgorithm() {
// Won't match any JCE algorithm.
return getKeyType();
}

@Override
public String getFormat() {
// We cannot produce an encoding for an unsupported key.
return null;
}

@Override
public byte[] getEncoded() {
// We cannot produce an encoding for an unsupported key.
return null;
}

@Override
public String getKeyType() {
return keyType;
}

/**
* Retrieves the raw key bytes (serialized form).
*
* @return the key bytes
*/
public byte[] getKeyData() {
return keyData.clone();
}

@Override
public int hashCode() {
return Arrays.hashCode(keyData) * 31 + Objects.hash(keyType);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof UnsupportedSshPublicKey)) {
return false;
}
UnsupportedSshPublicKey other = (UnsupportedSshPublicKey) obj;
return Arrays.equals(keyData, other.keyData) && Objects.equals(keyType, other.keyType);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

import java.security.PublicKey;

public interface SecurityKeyPublicKey<K extends PublicKey> extends PublicKey {
import org.apache.sshd.common.config.keys.SshPublicKey;

public interface SecurityKeyPublicKey<K extends PublicKey> extends SshPublicKey {
String getAppName();

boolean isNoTouchRequired();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Objects;

import net.i2p.crypto.eddsa.EdDSAPublicKey;
import org.apache.sshd.common.config.keys.impl.SkED25519PublicKeyEntryDecoder;

public class SkED25519PublicKey implements SecurityKeyPublicKey<EdDSAPublicKey> {

Expand All @@ -43,6 +44,11 @@ public String getAlgorithm() {
return ALGORITHM;
}

@Override
public String getKeyType() {
return SkED25519PublicKeyEntryDecoder.KEY_TYPE;
}

@Override
public String getFormat() {
return null;
Expand Down Expand Up @@ -99,4 +105,5 @@ public boolean equals(Object obj) {
&& this.noTouchRequired == other.noTouchRequired
&& Objects.equals(this.delegatePublicKey, other.delegatePublicKey);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.security.interfaces.ECPublicKey;
import java.util.Objects;

import org.apache.sshd.common.config.keys.impl.SkECDSAPublicKeyEntryDecoder;

public class SkEcdsaPublicKey implements SecurityKeyPublicKey<ECPublicKey> {

public static final String ALGORITHM = "ECDSA-SK";
Expand All @@ -42,6 +44,11 @@ public String getAlgorithm() {
return ALGORITHM;
}

@Override
public String getKeyType() {
return SkECDSAPublicKeyEntryDecoder.KEY_TYPE;
}

@Override
public String getFormat() {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import org.apache.sshd.common.cipher.ECCurves;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.OpenSshCertificate;
import org.apache.sshd.common.config.keys.UnsupportedSshPublicKey;
import org.apache.sshd.common.config.keys.u2f.SecurityKeyPublicKey;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.util.GenericUtils;
Expand Down Expand Up @@ -1015,7 +1016,9 @@ public void putRawPublicKey(PublicKey key) {

public void putRawPublicKeyBytes(PublicKey key) {
Objects.requireNonNull(key, "No key");
if (key instanceof RSAPublicKey) {
if (key instanceof UnsupportedSshPublicKey) {
putRawBytes(((UnsupportedSshPublicKey) key).getKeyData());
} else if (key instanceof RSAPublicKey) {
RSAPublicKey rsaPub = (RSAPublicKey) key;

putMPInt(rsaPub.getPublicExponent());
Expand Down
Loading

0 comments on commit 7cc9c49

Please sign in to comment.