SshAgentClient.java
/*
* Copyright (C) 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.agent;
import java.io.IOException;
import java.security.KeyPair;
import java.security.PublicKey;
import java.text.MessageFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sshd.agent.SshAgent;
import org.apache.sshd.agent.SshAgentConstants;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.BufferException;
import org.apache.sshd.common.util.buffer.BufferUtils;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.sshd.agent.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A client for an SSH2 agent. This client supports only querying identities and
* signature requests.
*
* @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH
* Agent Protocol, RFC draft</a>
*/
public class SshAgentClient implements SshAgent {
private static final Logger LOG = LoggerFactory
.getLogger(SshAgentClient.class);
// OpenSSH limit
private static final int MAX_NUMBER_OF_KEYS = 2048;
private final AtomicBoolean closed = new AtomicBoolean();
private final Connector connector;
/**
* Creates a new {@link SshAgentClient} implementing the SSH2 ssh agent
* protocol, using the given {@link Connector} to connect to the SSH agent
* and to exchange messages.
*
* @param connector
* {@link Connector} to use
*/
public SshAgentClient(Connector connector) {
this.connector = connector;
}
private boolean open(boolean debugging) throws IOException {
if (closed.get()) {
if (debugging) {
LOG.debug("SSH agent connection already closed"); //$NON-NLS-1$
}
return false;
}
boolean connected = connector != null && connector.connect();
if (!connected) {
if (debugging) {
LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$
}
}
return connected;
}
@Override
public void close() throws IOException {
if (!closed.getAndSet(true) && connector != null) {
connector.close();
}
}
@Override
public Iterable<? extends Map.Entry<PublicKey, String>> getIdentities()
throws IOException {
boolean debugging = LOG.isDebugEnabled();
if (!open(debugging)) {
return Collections.emptyList();
}
if (debugging) {
LOG.debug("Requesting identities from SSH agent"); //$NON-NLS-1$
}
try {
Buffer reply = rpc(
SshAgentConstants.SSH2_AGENTC_REQUEST_IDENTITIES);
byte cmd = reply.getByte();
if (cmd != SshAgentConstants.SSH2_AGENT_IDENTITIES_ANSWER) {
throw new SshException(MessageFormat.format(
SshdText.get().sshAgentReplyUnexpected,
SshAgentConstants.getCommandMessageName(cmd)));
}
int numberOfKeys = reply.getInt();
if (numberOfKeys < 0 || numberOfKeys > MAX_NUMBER_OF_KEYS) {
throw new SshException(MessageFormat.format(
SshdText.get().sshAgentWrongNumberOfKeys,
Integer.toString(numberOfKeys)));
}
if (numberOfKeys == 0) {
if (debugging) {
LOG.debug("SSH agent has no keys"); //$NON-NLS-1$
}
return Collections.emptyList();
}
if (debugging) {
LOG.debug("Got {} key(s) from the SSH agent", //$NON-NLS-1$
Integer.toString(numberOfKeys));
}
boolean tracing = LOG.isTraceEnabled();
List<Map.Entry<PublicKey, String>> keys = new ArrayList<>(
numberOfKeys);
for (int i = 0; i < numberOfKeys; i++) {
PublicKey key = reply.getPublicKey();
String comment = reply.getString();
if (tracing) {
LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$
KeyUtils.getKeyType(key),
KeyUtils.getFingerPrint(key), comment);
}
keys.add(new AbstractMap.SimpleImmutableEntry<>(key, comment));
}
return keys;
} catch (BufferException e) {
throw new SshException(SshdText.get().sshAgentShortReadBuffer, e);
}
}
@Override
public Map.Entry<String, byte[]> sign(SessionContext session, PublicKey key,
String algorithm, byte[] data) throws IOException {
boolean debugging = LOG.isDebugEnabled();
String keyType = KeyUtils.getKeyType(key);
String signatureAlgorithm;
if (algorithm != null) {
if (!KeyUtils.getCanonicalKeyType(algorithm).equals(keyType)) {
throw new IllegalArgumentException(MessageFormat.format(
SshdText.get().invalidSignatureAlgorithm, algorithm,
keyType));
}
signatureAlgorithm = algorithm;
} else {
signatureAlgorithm = keyType;
}
if (!open(debugging)) {
return null;
}
int flags = 0;
switch (signatureAlgorithm) {
case KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS:
case KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS:
flags = 4;
break;
case KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS:
case KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS:
flags = 2;
break;
default:
break;
}
ByteArrayBuffer msg = new ByteArrayBuffer();
msg.putInt(0);
msg.putByte(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST);
msg.putPublicKey(key);
msg.putBytes(data);
msg.putInt(flags);
if (debugging) {
LOG.debug(
"sign({}): signing request to SSH agent for {} key, {} signature; flags={}", //$NON-NLS-1$
session, keyType, signatureAlgorithm,
Integer.toString(flags));
}
Buffer reply = rpc(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST,
msg.getCompactData());
byte cmd = reply.getByte();
if (cmd != SshAgentConstants.SSH2_AGENT_SIGN_RESPONSE) {
throw new SshException(
MessageFormat.format(SshdText.get().sshAgentReplyUnexpected,
SshAgentConstants.getCommandMessageName(cmd)));
}
try {
Buffer signatureReply = new ByteArrayBuffer(reply.getBytes());
String actualAlgorithm = signatureReply.getString();
byte[] signature = signatureReply.getBytes();
if (LOG.isTraceEnabled()) {
LOG.trace(
"sign({}): signature reply from SSH agent for {} key: {} signature={}", //$NON-NLS-1$
session, keyType, actualAlgorithm,
BufferUtils.toHex(':', signature));
} else if (LOG.isDebugEnabled()) {
LOG.debug(
"sign({}): signature reply from SSH agent for {} key, {} signature", //$NON-NLS-1$
session, keyType, actualAlgorithm);
}
return new AbstractMap.SimpleImmutableEntry<>(actualAlgorithm,
signature);
} catch (BufferException e) {
throw new SshException(SshdText.get().sshAgentShortReadBuffer, e);
}
}
private Buffer rpc(byte command, byte[] message) throws IOException {
return new ByteArrayBuffer(connector.rpc(command, message));
}
private Buffer rpc(byte command) throws IOException {
return new ByteArrayBuffer(connector.rpc(command));
}
@Override
public boolean isOpen() {
return !closed.get();
}
@Override
public void addIdentity(KeyPair key, String comment) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void removeIdentity(PublicKey key) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void removeAllIdentities() throws IOException {
throw new UnsupportedOperationException();
}
}