SshAgentClient.java

  1. /*
  2.  * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.internal.transport.sshd.agent;

  11. import java.io.IOException;
  12. import java.security.KeyPair;
  13. import java.security.PrivateKey;
  14. import java.security.PublicKey;
  15. import java.text.MessageFormat;
  16. import java.util.AbstractMap;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.Collections;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.concurrent.atomic.AtomicBoolean;

  23. import org.apache.sshd.agent.SshAgent;
  24. import org.apache.sshd.agent.SshAgentConstants;
  25. import org.apache.sshd.agent.SshAgentKeyConstraint;
  26. import org.apache.sshd.common.SshException;
  27. import org.apache.sshd.common.config.keys.KeyUtils;
  28. import org.apache.sshd.common.keyprovider.KeyPairProvider;
  29. import org.apache.sshd.common.session.SessionContext;
  30. import org.apache.sshd.common.util.buffer.Buffer;
  31. import org.apache.sshd.common.util.buffer.BufferException;
  32. import org.apache.sshd.common.util.buffer.BufferUtils;
  33. import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  34. import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser;
  35. import org.apache.sshd.common.util.io.der.DERParser;
  36. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  37. import org.eclipse.jgit.transport.sshd.agent.Connector;
  38. import org.slf4j.Logger;
  39. import org.slf4j.LoggerFactory;

  40. /**
  41.  * A client for an SSH2 agent. This client supports querying identities,
  42.  * signature requests, and adding keys to an agent (with or without
  43.  * constraints). Removing keys is not supported, and the older SSH1 protocol is
  44.  * not supported.
  45.  *
  46.  * @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH
  47.  *      Agent Protocol, RFC draft</a>
  48.  */
  49. public class SshAgentClient implements SshAgent {

  50.     private static final Logger LOG = LoggerFactory
  51.             .getLogger(SshAgentClient.class);

  52.     // OpenSSH limit
  53.     private static final int MAX_NUMBER_OF_KEYS = 2048;

  54.     private final AtomicBoolean closed = new AtomicBoolean();

  55.     private final Connector connector;

  56.     /**
  57.      * Creates a new {@link SshAgentClient} implementing the SSH2 ssh agent
  58.      * protocol, using the given {@link Connector} to connect to the SSH agent
  59.      * and to exchange messages.
  60.      *
  61.      * @param connector
  62.      *            {@link Connector} to use
  63.      */
  64.     public SshAgentClient(Connector connector) {
  65.         this.connector = connector;
  66.     }

  67.     private boolean open(boolean debugging) throws IOException {
  68.         if (closed.get()) {
  69.             if (debugging) {
  70.                 LOG.debug("SSH agent connection already closed"); //$NON-NLS-1$
  71.             }
  72.             return false;
  73.         }
  74.         boolean connected;
  75.         try {
  76.             connected = connector != null && connector.connect();
  77.             if (!connected && debugging) {
  78.                 LOG.debug("No SSH agent"); //$NON-NLS-1$
  79.             }
  80.         } catch (IOException e) {
  81.             // Agent not running?
  82.             if (debugging) {
  83.                 LOG.debug("No SSH agent", e); //$NON-NLS-1$
  84.             }
  85.             throw e;
  86.         }
  87.         return connected;
  88.     }

  89.     @Override
  90.     public void close() throws IOException {
  91.         if (!closed.getAndSet(true) && connector != null) {
  92.             connector.close();
  93.         }
  94.     }

  95.     @Override
  96.     public Iterable<? extends Map.Entry<PublicKey, String>> getIdentities()
  97.             throws IOException {
  98.         boolean debugging = LOG.isDebugEnabled();
  99.         if (!open(debugging)) {
  100.             return Collections.emptyList();
  101.         }
  102.         if (debugging) {
  103.             LOG.debug("Requesting identities from SSH agent"); //$NON-NLS-1$
  104.         }
  105.         try {
  106.             Buffer reply = rpc(
  107.                     SshAgentConstants.SSH2_AGENTC_REQUEST_IDENTITIES);
  108.             byte cmd = reply.getByte();
  109.             if (cmd != SshAgentConstants.SSH2_AGENT_IDENTITIES_ANSWER) {
  110.                 throw new SshException(MessageFormat.format(
  111.                         SshdText.get().sshAgentReplyUnexpected,
  112.                         SshAgentConstants.getCommandMessageName(cmd)));
  113.             }
  114.             int numberOfKeys = reply.getInt();
  115.             if (numberOfKeys < 0 || numberOfKeys > MAX_NUMBER_OF_KEYS) {
  116.                 throw new SshException(MessageFormat.format(
  117.                         SshdText.get().sshAgentWrongNumberOfKeys,
  118.                         Integer.toString(numberOfKeys)));
  119.             }
  120.             if (numberOfKeys == 0) {
  121.                 if (debugging) {
  122.                     LOG.debug("SSH agent has no keys"); //$NON-NLS-1$
  123.                 }
  124.                 return Collections.emptyList();
  125.             }
  126.             if (debugging) {
  127.                 LOG.debug("Got {} key(s) from the SSH agent", //$NON-NLS-1$
  128.                         Integer.toString(numberOfKeys));
  129.             }
  130.             boolean tracing = LOG.isTraceEnabled();
  131.             List<Map.Entry<PublicKey, String>> keys = new ArrayList<>(
  132.                     numberOfKeys);
  133.             for (int i = 0; i < numberOfKeys; i++) {
  134.                 PublicKey key = readKey(reply);
  135.                 String comment = reply.getString();
  136.                 if (key != null) {
  137.                     if (tracing) {
  138.                         LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$
  139.                                 KeyUtils.getKeyType(key),
  140.                                 KeyUtils.getFingerPrint(key), comment);
  141.                     }
  142.                     keys.add(new AbstractMap.SimpleImmutableEntry<>(key,
  143.                             comment));
  144.                 }
  145.             }
  146.             return keys;
  147.         } catch (BufferException e) {
  148.             throw new SshException(SshdText.get().sshAgentShortReadBuffer, e);
  149.         }
  150.     }

  151.     @Override
  152.     public Map.Entry<String, byte[]> sign(SessionContext session, PublicKey key,
  153.             String algorithm, byte[] data) throws IOException {
  154.         boolean debugging = LOG.isDebugEnabled();
  155.         String keyType = KeyUtils.getKeyType(key);
  156.         String signatureAlgorithm;
  157.         if (algorithm != null) {
  158.             if (!KeyUtils.getCanonicalKeyType(algorithm).equals(keyType)) {
  159.                 throw new IllegalArgumentException(MessageFormat.format(
  160.                         SshdText.get().invalidSignatureAlgorithm, algorithm,
  161.                         keyType));
  162.             }
  163.             signatureAlgorithm = algorithm;
  164.         } else {
  165.             signatureAlgorithm = keyType;
  166.         }
  167.         if (!open(debugging)) {
  168.             return null;
  169.         }
  170.         int flags = 0;
  171.         switch (signatureAlgorithm) {
  172.         case KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS:
  173.         case KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS:
  174.             flags = 4;
  175.             break;
  176.         case KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS:
  177.         case KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS:
  178.             flags = 2;
  179.             break;
  180.         default:
  181.             break;
  182.         }
  183.         ByteArrayBuffer msg = new ByteArrayBuffer();
  184.         msg.putInt(0);
  185.         msg.putByte(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST);
  186.         msg.putPublicKey(key);
  187.         msg.putBytes(data);
  188.         msg.putInt(flags);
  189.         if (debugging) {
  190.             LOG.debug(
  191.                     "sign({}): signing request to SSH agent for {} key, {} signature; flags={}", //$NON-NLS-1$
  192.                     session, keyType, signatureAlgorithm,
  193.                     Integer.toString(flags));
  194.         }
  195.         Buffer reply = rpc(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST,
  196.                 msg.getCompactData());
  197.         byte cmd = reply.getByte();
  198.         if (cmd != SshAgentConstants.SSH2_AGENT_SIGN_RESPONSE) {
  199.             throw new SshException(
  200.                     MessageFormat.format(SshdText.get().sshAgentReplyUnexpected,
  201.                             SshAgentConstants.getCommandMessageName(cmd)));
  202.         }
  203.         try {
  204.             Buffer signatureReply = new ByteArrayBuffer(reply.getBytes());
  205.             String actualAlgorithm = signatureReply.getString();
  206.             byte[] signature = signatureReply.getBytes();
  207.             if (LOG.isTraceEnabled()) {
  208.                 LOG.trace(
  209.                         "sign({}): signature reply from SSH agent for {} key: {} signature={}", //$NON-NLS-1$
  210.                         session, keyType, actualAlgorithm,
  211.                         BufferUtils.toHex(':', signature));

  212.             } else if (LOG.isDebugEnabled()) {
  213.                 LOG.debug(
  214.                         "sign({}): signature reply from SSH agent for {} key, {} signature", //$NON-NLS-1$
  215.                         session, keyType, actualAlgorithm);
  216.             }
  217.             return new AbstractMap.SimpleImmutableEntry<>(actualAlgorithm,
  218.                     signature);
  219.         } catch (BufferException e) {
  220.             throw new SshException(SshdText.get().sshAgentShortReadBuffer, e);
  221.         }
  222.     }

  223.     @Override
  224.     public void addIdentity(KeyPair key, String comment,
  225.             SshAgentKeyConstraint... constraints) throws IOException {
  226.         boolean debugging = LOG.isDebugEnabled();
  227.         if (!open(debugging)) {
  228.             return;
  229.         }

  230.         // Neither Pageant 0.76 nor Win32-OpenSSH 8.6 support command
  231.         // SSH2_AGENTC_ADD_ID_CONSTRAINED. Adding a key with constraints will
  232.         // fail. The only work-around for users is not to use "confirm" or "time
  233.         // spec" with AddKeysToAgent, and not to use sk-* keys.
  234.         //
  235.         // With a true OpenSSH SSH agent, key constraints work.
  236.         byte cmd = (constraints != null && constraints.length > 0)
  237.                 ? SshAgentConstants.SSH2_AGENTC_ADD_ID_CONSTRAINED
  238.                 : SshAgentConstants.SSH2_AGENTC_ADD_IDENTITY;
  239.         byte[] message = null;
  240.         ByteArrayBuffer msg = new ByteArrayBuffer();
  241.         try {
  242.             msg.putInt(0);
  243.             msg.putByte(cmd);
  244.             String keyType = KeyUtils.getKeyType(key);
  245.             if (KeyPairProvider.SSH_ED25519.equals(keyType)) {
  246.                 // Apache MINA sshd 2.8.0 lacks support for writing ed25519
  247.                 // private keys to a buffer.
  248.                 putEd25519Key(msg, key);
  249.             } else {
  250.                 msg.putKeyPair(key);
  251.             }
  252.             msg.putString(comment == null ? "" : comment); //$NON-NLS-1$
  253.             if (constraints != null) {
  254.                 for (SshAgentKeyConstraint constraint : constraints) {
  255.                     constraint.put(msg);
  256.                 }
  257.             }
  258.             if (debugging) {
  259.                 LOG.debug(
  260.                         "addIdentity: adding {} key {} to SSH agent; comment {}", //$NON-NLS-1$
  261.                         keyType, KeyUtils.getFingerPrint(key.getPublic()),
  262.                         comment);
  263.             }
  264.             message = msg.getCompactData();
  265.         } finally {
  266.             // The message contains the private key data, so clear intermediary
  267.             // data ASAP.
  268.             msg.clear();
  269.         }
  270.         Buffer reply;
  271.         try {
  272.             reply = rpc(cmd, message);
  273.         } finally {
  274.             Arrays.fill(message, (byte) 0);
  275.         }
  276.         int replyLength = reply.available();
  277.         if (replyLength != 1) {
  278.             throw new SshException(MessageFormat.format(
  279.                     SshdText.get().sshAgentReplyUnexpected,
  280.                     MessageFormat.format(
  281.                             SshdText.get().sshAgentPayloadLengthError,
  282.                             Integer.valueOf(1), Integer.valueOf(replyLength))));

  283.         }
  284.         cmd = reply.getByte();
  285.         if (cmd != SshAgentConstants.SSH_AGENT_SUCCESS) {
  286.             throw new SshException(
  287.                     MessageFormat.format(SshdText.get().sshAgentReplyUnexpected,
  288.                             SshAgentConstants.getCommandMessageName(cmd)));
  289.         }
  290.     }

  291.     /**
  292.      * Writes an ed25519 {@link KeyPair} to a {@link Buffer}. OpenSSH specifies
  293.      * that it expects the 32 public key bytes, followed by 64 bytes formed by
  294.      * concatenating the 32 private key bytes with the 32 public key bytes.
  295.      *
  296.      * @param msg
  297.      *            {@link Buffer} to write to
  298.      * @param key
  299.      *            {@link KeyPair} to write
  300.      * @throws IOException
  301.      *             if the private key cannot be written
  302.      */
  303.     private static void putEd25519Key(Buffer msg, KeyPair key)
  304.             throws IOException {
  305.         Buffer tmp = new ByteArrayBuffer(36);
  306.         tmp.putRawPublicKeyBytes(key.getPublic());
  307.         byte[] publicBytes = tmp.getBytes();
  308.         msg.putString(KeyPairProvider.SSH_ED25519);
  309.         msg.putBytes(publicBytes);
  310.         // Next is the concatenation of the 32 byte private key value with the
  311.         // 32 bytes of the public key.
  312.         PrivateKey pk = key.getPrivate();
  313.         String format = pk.getFormat();
  314.         if (!"PKCS#8".equalsIgnoreCase(format)) { //$NON-NLS-1$
  315.             throw new IOException(MessageFormat
  316.                     .format(SshdText.get().sshAgentEdDSAFormatError, format));
  317.         }
  318.         byte[] privateBytes = null;
  319.         byte[] encoded = pk.getEncoded();
  320.         try {
  321.             privateBytes = asn1Parse(encoded, 32);
  322.             byte[] combined = Arrays.copyOf(privateBytes, 64);
  323.             Arrays.fill(privateBytes, (byte) 0);
  324.             privateBytes = combined;
  325.             System.arraycopy(publicBytes, 0, privateBytes, 32, 32);
  326.             msg.putBytes(privateBytes);
  327.         } finally {
  328.             if (privateBytes != null) {
  329.                 Arrays.fill(privateBytes, (byte) 0);
  330.             }
  331.             Arrays.fill(encoded, (byte) 0);
  332.         }
  333.     }

  334.     /**
  335.      * Extracts the private key bytes from an encoded ed25519 private key by
  336.      * parsing the bytes as ASN.1 according to RFC 5958 (PKCS #8 encoding):
  337.      *
  338.      * <pre>
  339.      * OneAsymmetricKey ::= SEQUENCE {
  340.      *   version Version,
  341.      *   privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
  342.      *   privateKey PrivateKey,
  343.      *   ...
  344.      * }
  345.      *
  346.      * Version ::= INTEGER
  347.      * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
  348.      * PrivateKey ::= OCTET STRING
  349.      *
  350.      * AlgorithmIdentifier  ::=  SEQUENCE  {
  351.      *   algorithm   OBJECT IDENTIFIER,
  352.      *   parameters  ANY DEFINED BY algorithm OPTIONAL
  353.      * }
  354.      * </pre>
  355.      * <p>
  356.      * and RFC 8410: "... when encoding a OneAsymmetricKey object, the private
  357.      * key is wrapped in a CurvePrivateKey object and wrapped by the OCTET
  358.      * STRING of the 'privateKey' field."
  359.      * </p>
  360.      *
  361.      * <pre>
  362.      * CurvePrivateKey ::= OCTET STRING
  363.      * </pre>
  364.      *
  365.      * @param encoded
  366.      *            encoded private key to extract the private key bytes from
  367.      * @param n
  368.      *            number of bytes expected
  369.      * @return the extracted private key bytes; of length {@code n}
  370.      * @throws IOException
  371.      *             if the private key cannot be extracted
  372.      * @see <a href="https://tools.ietf.org/html/rfc5958">RFC 5958</a>
  373.      * @see <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a>
  374.      */
  375.     private static byte[] asn1Parse(byte[] encoded, int n) throws IOException {
  376.         byte[] privateKey = null;
  377.         try (DERParser byteParser = new DERParser(encoded);
  378.                 DERParser oneAsymmetricKey = byteParser.readObject()
  379.                         .createParser()) {
  380.             oneAsymmetricKey.readObject(); // skip version
  381.             oneAsymmetricKey.readObject(); // skip algorithm identifier
  382.             privateKey = oneAsymmetricKey.readObject().getValue();
  383.             // The last n bytes of this must be the private key bytes
  384.             return Arrays.copyOfRange(privateKey,
  385.                     privateKey.length - n, privateKey.length);
  386.         } finally {
  387.             if (privateKey != null) {
  388.                 Arrays.fill(privateKey, (byte) 0);
  389.             }
  390.         }
  391.     }

  392.     /**
  393.      * A safe version of {@link Buffer#getPublicKey()}. Upon return the
  394.      * buffers's read position is always after the key blob; any exceptions
  395.      * thrown by trying to read the key are logged and <em>not</em> propagated.
  396.      * <p>
  397.      * This is needed because an SSH agent might contain and deliver keys that
  398.      * we cannot handle (for instance ed448 keys).
  399.      * </p>
  400.      *
  401.      * @param buffer
  402.      *            to read the key from
  403.      * @return the {@link PublicKey}, or {@code null} if the key could not be
  404.      *         read
  405.      * @throws BufferException
  406.      *             if the length of the key blob cannot be read or is corrupted
  407.      */
  408.     private static PublicKey readKey(Buffer buffer) throws BufferException {
  409.         int endOfBuffer = buffer.wpos();
  410.         int keyLength = buffer.getInt();
  411.         if (keyLength <= 0 || keyLength > buffer.available()) {
  412.             throw new BufferException(
  413.                     MessageFormat.format(SshdText.get().sshAgentWrongKeyLength,
  414.                             Integer.toString(keyLength),
  415.                             Integer.toString(buffer.rpos()),
  416.                             Integer.toString(endOfBuffer)));
  417.         }
  418.         int afterKey = buffer.rpos() + keyLength;
  419.         // Limit subsequent reads to the public key blob
  420.         buffer.wpos(afterKey);
  421.         try {
  422.             return buffer.getRawPublicKey(BufferPublicKeyParser.DEFAULT);
  423.         } catch (Exception e) {
  424.             LOG.warn(SshdText.get().sshAgentUnknownKey, e);
  425.             return null;
  426.         } finally {
  427.             // Restore real buffer end
  428.             buffer.wpos(endOfBuffer);
  429.             // Set the read position to after this key, even if failed
  430.             buffer.rpos(afterKey);
  431.         }
  432.     }

  433.     private Buffer rpc(byte command, byte[] message) throws IOException {
  434.         return new ByteArrayBuffer(connector.rpc(command, message));
  435.     }

  436.     private Buffer rpc(byte command) throws IOException {
  437.         return new ByteArrayBuffer(connector.rpc(command));
  438.     }

  439.     @Override
  440.     public boolean isOpen() {
  441.         return !closed.get();
  442.     }

  443.     @Override
  444.     public void removeIdentity(PublicKey key) throws IOException {
  445.         throw new UnsupportedOperationException();
  446.     }

  447.     @Override
  448.     public void removeAllIdentities() throws IOException {
  449.         throw new UnsupportedOperationException();
  450.     }
  451. }