blob: 675509442024a7314d180723afcf3f6580106ef5 [file] [log] [blame]
/*
* Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.transport.sshd;
import java.io.IOException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.text.MessageFormat;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.RuntimeSshException;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.signature.Signature;
import org.apache.sshd.common.signature.SignatureFactoriesHolder;
import org.apache.sshd.common.util.buffer.Buffer;
/**
* Custom {@link UserAuthPublicKey} implementation fixing SSHD-1105: if there
* are several signature algorithms applicable for a public key type, we must
* try them all, in the correct order.
*
* @see <a href="https://issues.apache.org/jira/browse/SSHD-1105">SSHD-1105</a>
* @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=572056">Bug
* 572056</a>
*/
public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
private final Deque<String> currentAlgorithms = new LinkedList<>();
private String chosenAlgorithm;
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
super(factories);
}
@Override
protected boolean sendAuthDataRequest(ClientSession session, String service)
throws Exception {
if (current == null) {
currentAlgorithms.clear();
chosenAlgorithm = null;
}
String currentAlgorithm = null;
if (current != null && !currentAlgorithms.isEmpty()) {
currentAlgorithm = currentAlgorithms.poll();
if (chosenAlgorithm != null) {
Set<String> knownServerAlgorithms = session.getAttribute(
JGitKexExtensionHandler.SERVER_ALGORITHMS);
if (knownServerAlgorithms != null
&& knownServerAlgorithms.contains(chosenAlgorithm)) {
// We've tried key 'current' with 'chosenAlgorithm', but it
// failed. However, the server had told us it supported
// 'chosenAlgorithm'. Thus it makes no sense to continue
// with this key and other signature algorithms. Skip to the
// next key, if any.
currentAlgorithm = null;
}
}
}
if (currentAlgorithm == null) {
try {
if (keys == null || !keys.hasNext()) {
if (log.isDebugEnabled()) {
log.debug(
"sendAuthDataRequest({})[{}] no more keys to send", //$NON-NLS-1$
session, service);
}
current = null;
return false;
}
current = keys.next();
currentAlgorithms.clear();
chosenAlgorithm = null;
} catch (Error e) { // Copied from superclass
throw new RuntimeSshException(e);
}
}
PublicKey key;
try {
key = current.getPublicKey();
} catch (Error e) { // Copied from superclass
throw new RuntimeSshException(e);
}
if (currentAlgorithm == null) {
String keyType = KeyUtils.getKeyType(key);
Set<String> aliases = new HashSet<>(
KeyUtils.getAllEquivalentKeyTypes(keyType));
aliases.add(keyType);
List<NamedFactory<Signature>> existingFactories;
if (current instanceof SignatureFactoriesHolder) {
existingFactories = ((SignatureFactoriesHolder) current)
.getSignatureFactories();
} else {
existingFactories = getSignatureFactories();
}
if (existingFactories != null) {
if (log.isDebugEnabled()) {
log.debug(
"sendAuthDataRequest({})[{}] selecting from PubKeyAcceptedAlgorithms {}", //$NON-NLS-1$
session, service,
NamedResource.getNames(existingFactories));
}
// Select the factories by name and in order
existingFactories.forEach(f -> {
if (aliases.contains(f.getName())) {
currentAlgorithms.add(f.getName());
}
});
}
currentAlgorithm = currentAlgorithms.isEmpty() ? keyType
: currentAlgorithms.poll();
}
String name = getName();
if (log.isDebugEnabled()) {
log.debug(
"sendAuthDataRequest({})[{}] send SSH_MSG_USERAUTH_REQUEST request {} type={} - fingerprint={}", //$NON-NLS-1$
session, service, name, currentAlgorithm,
KeyUtils.getFingerPrint(key));
}
chosenAlgorithm = currentAlgorithm;
Buffer buffer = session
.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
buffer.putString(session.getUsername());
buffer.putString(service);
buffer.putString(name);
buffer.putBoolean(false);
buffer.putString(currentAlgorithm);
buffer.putPublicKey(key);
session.writePacket(buffer);
return true;
}
@Override
protected boolean processAuthDataRequest(ClientSession session,
String service, Buffer buffer) throws Exception {
String name = getName();
int cmd = buffer.getUByte();
if (cmd != SshConstants.SSH_MSG_USERAUTH_PK_OK) {
throw new IllegalStateException(MessageFormat.format(
SshdText.get().pubkeyAuthWrongCommand,
SshConstants.getCommandMessageName(cmd),
session.getConnectAddress(), session.getServerVersion()));
}
PublicKey key;
try {
key = current.getPublicKey();
} catch (Error e) { // Copied from superclass
throw new RuntimeSshException(e);
}
String rspKeyAlgorithm = buffer.getString();
PublicKey rspKey = buffer.getPublicKey();
if (log.isDebugEnabled()) {
log.debug(
"processAuthDataRequest({})[{}][{}] SSH_MSG_USERAUTH_PK_OK type={}, fingerprint={}", //$NON-NLS-1$
session, service, name, rspKeyAlgorithm,
KeyUtils.getFingerPrint(rspKey));
}
if (!KeyUtils.compareKeys(rspKey, key)) {
throw new InvalidKeySpecException(MessageFormat.format(
SshdText.get().pubkeyAuthWrongKey,
KeyUtils.getFingerPrint(key),
KeyUtils.getFingerPrint(rspKey),
session.getConnectAddress(), session.getServerVersion()));
}
if (!chosenAlgorithm.equalsIgnoreCase(rspKeyAlgorithm)) {
// 'algo' SHOULD be the same as 'chosenAlgorithm', which is the one
// we sent above. See https://tools.ietf.org/html/rfc4252#page-9 .
//
// However, at least Github (SSH-2.0-babeld-383743ad) servers seem
// to return the key type, not the algorithm name.
//
// So we don't check but just log the inconsistency. We sign using
// 'chosenAlgorithm' in any case, so we don't really care what the
// server says here.
log.warn(MessageFormat.format(
SshdText.get().pubkeyAuthWrongSignatureAlgorithm,
chosenAlgorithm, rspKeyAlgorithm, session.getConnectAddress(),
session.getServerVersion()));
}
String username = session.getUsername();
Buffer out = session
.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
out.putString(username);
out.putString(service);
out.putString(name);
out.putBoolean(true);
out.putString(chosenAlgorithm);
out.putPublicKey(key);
if (log.isDebugEnabled()) {
log.debug(
"processAuthDataRequest({})[{}][{}]: signing with algorithm {}", //$NON-NLS-1$
session, service, name, chosenAlgorithm);
}
appendSignature(session, service, name, username, chosenAlgorithm, key,
out);
session.writePacket(out);
return true;
}
@Override
protected void releaseKeys() throws IOException {
currentAlgorithms.clear();
current = null;
chosenAlgorithm = null;
super.releaseKeys();
}
}