/*

  ssh-signer2.c

  Author: Sami Lehtinen <sjl@ssh.com>

  Copyright (C) 1999-2000 SSH Communications Security Corp, Helsinki, Finland
  All rights reserved.

  Hostbased authentication, client-side. This program is supposed to
  be suid, as it should have access to the private part of the host
  key.

*/
/*
  ssh-signer2 communicates with the ssh2-client with a very simple protocol.

  First, ssh2 will fork and exec ssh-signer2. Then it wraps
  ssh-signer2's stdin and stdout to a sshpipestream. This stream is
  then wrapped to a sshpacketstream. Using this stream it is very easy
  to implement a packet-based protocol.

  Possible messages from ssh2 to ssh-signer2:

  #define SSH_AUTH_HOSTBASED_PACKET    (SshPacketType)1

    If this is received, ssh-signer2 begins to check for the packet's
    validity. As ssh-signer2 has (should have) access to the client
    host's hostkey, it shouldn't sign everything it
    receives. Particularly the client host's name and user's name in
    the client host are important. This packet MUST be the first, or
    the next right after SSH_AUTH_HOSTBASED_COMPAT packet.

    This messages payload is packet, which is formatted according to
    the ssh-userauth draft, under hostbased-authentication.

  #define SSH_AUTH_HOSTBASED_COMPAT    (SshPacketType)5

    This MUST be sent first, if at all. Packet payload is a
    comma-separated list of compatibility flags needed to interoperate
    with older versions of servers.
    
  #define SSH_AUTH_HOSTBASED_END       (SshPacketType)2

    This notifies ssh-signer2 that ssh2 client no longer needs ssh-signer2.
    This message has no payload.
    
  Possible messages from ssh-signer2 to ssh2:

  #define SSH_AUTH_HOSTBASED_SIGNATURE (SshPacketType)3

    This message is sent by ssh-signer2 to ssh2 when it has checked
    the packet and signed it. Payload is the signature.

  Common messages:
  
  #define SSH_AUTH_HOSTBASED_ERROR     (SshPacketType)4

    Payload:
        byte    error_code (type SshAuthHostbasedError)
        string  human readable error message
        string  language tag (as per [RFC-1776])

     After this packet nothing else should sent to the stream, and
     both should close it.
*/

#include "ssh2includes.h"
#include "sshpacketstream.h"
#include "ssheloop.h"
#include "sshfdstream.h"
#include "sshencode.h"
#include "ssh-signer2.h"
#include "sshmsgs.h"
#include "sshauth.h"
#include "sshconn.h"
#include "sshconfig.h"
#include "sshuserfiles.h"
#include "ssh2pubkeyencode.h"
#include "sshtcp.h"
#include "sshconfig.h"
#include "sshtimeouts.h"
#include "sshdsprintf.h"
#include "sshappcommon.h"
#ifdef HAVE_SYS_UTSNAME_H
#include <sys/utsname.h>
#endif /* HAVE_SYS_UTSNAME_H */
#include "sshfsm.h"

#define SSH_DEBUG_MODULE "SshSigner2"

/* Define this to get hexdumps */
/* #define HEXDUMPS */

/* Define this to get debug messages */
/* #define SIGNER_DEBUG */

/* Define this to make ssh-signer2 sleep for 30 seconds after it's
   start. (Only useful in debugging.)*/
/* #define SLEEP_AFTER_STARTUP */

/* Define this to make ssh-signer2 really quiet. */
#define SIGNER_QUIET

/* Stored program name for ssh-signer (taken from argv[0]). */
static char *progname;

typedef enum
{
  SIGNER_START = 0,
  SIGNER_COMPAT_FLAGS_RECEIVED,
  SIGNER_HOSTBASED_PACKET_RECEIVED,
  SIGNER_DEAD
} SignerState;

typedef struct SignerCompatRec
{
  Boolean hostbased_requested_service_draft_incompat;
  Boolean signature_encode_draft_incompat;
} *SignerCompat;

typedef struct SshSignerRec
{
  /* Parameters for callbacks*/
  unsigned char *packet_payload;
  size_t packet_payload_len;

  SshPacketType packet_type;

  Boolean packet_pending;
  
  /* Internal stuff */
  SshConfig serv_config;
  SshConfig global_config;
  SshUser effective_user_data;
  SshUser real_user;
  Boolean quiet;
  SshPacketWrapper wrapper;
  SshFSM fsm;
  SshFSMThread main_thread;
  SignerState state;
  SshAuthHostBasedError error_code;

  /* Operation handle for the async signing operation. */
  SshOperationHandle op_handle;
  
  /* compat flags. */
  SignerCompat compat;

  char *pubkeytype;
  
  /* If we need to send an error message, this field will contain the
     "human readable error string". */
  char *error_message_to_ssh2;
  /* ... and this will contain the error code.  */
  SshAuthHostBasedError error_code_to_ssh2;
} *SshSigner;

/* Forward declarations. */
void signer_can_send(void *context);

SSH_FSM_STEP(signer_suspend)
{
  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(signer_process_error_packet)
{
  unsigned int error_code;
  char *error_message = NULL, *lang_tag = NULL;
  size_t parsed_len = 0L;
  
  SSH_FSM_GDATA(SshSigner);

  SSH_FSM_SET_NEXT("signer_finish");

  /* Tell packet wrapper we don't want more packets, and output EOF to
     the stream. */
  ssh_packet_wrapper_send_eof(gdata->wrapper);
  
  if ((parsed_len = ssh_decode_array(gdata->packet_payload,
                                     gdata->packet_payload_len,
                                     SSH_FORMAT_CHAR, &error_code,
                                     SSH_FORMAT_UINT32_STR,
                                     &error_message, NULL,
                                     SSH_FORMAT_UINT32_STR, &lang_tag, NULL,
                                     SSH_FORMAT_END)) == 0)
    {
      ssh_warning("Invalid error packet received (what an irony...).");      
      gdata->error_code = SIGNER_ERROR_PROTOCOL_ERROR;
    }

  if (parsed_len < gdata->packet_payload_len)
    {
      ssh_warning("Extra data after SSH_AUTH_HOSTBASED_ERROR packet.");
    }

  ssh_warning("Received error %d, message \"%s\", lang_tag \"%s\".",
              error_code, error_message, lang_tag);
              
  switch (error_code)
    {
    case SIGNER_ERROR_GENERIC:
    case SIGNER_ERROR_PROTOCOL_ERROR:
      gdata->error_code = error_code;
      break;
    default:
      ssh_warning("Received unknown error code %d.", error_code);
      gdata->error_code = SIGNER_ERROR_PROTOCOL_ERROR;
    }

  ssh_xfree(error_message);
  ssh_xfree(lang_tag);
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(signer_process_end)
{
  SSH_FSM_SET_NEXT("signer_finish");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(signer_send_error)
{
  SSH_FSM_GDATA(SshSigner);

  gdata->state = SIGNER_DEAD;

  if (gdata->packet_payload)
    {
      ssh_xfree(gdata->packet_payload);
      gdata->packet_payload = NULL;
    }

  gdata->packet_type = SSH_AUTH_HOSTBASED_ERROR;
  
  gdata->packet_payload_len =
    ssh_encode_array_alloc(&gdata->packet_payload,
                           SSH_FORMAT_CHAR,
                           (unsigned int) gdata->error_code_to_ssh2,
                           SSH_FORMAT_UINT32_STR, gdata->error_message_to_ssh2,
                           strlen(gdata->error_message_to_ssh2),
                           SSH_FORMAT_UINT32_STR, "en", 2,
                           SSH_FORMAT_END);

  ssh_packet_wrapper_can_receive(gdata->wrapper, FALSE);

  SSH_FSM_SET_NEXT("signer_finish");

  gdata->packet_pending = TRUE;
  
  if (ssh_packet_wrapper_can_send(gdata->wrapper))
    ssh_register_timeout(0L, 0L, signer_can_send, gdata);
  
  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(signer_check_hostbased_packet)
{
  /* Received. */
  unsigned int msg_byte; /* This is unsigned int because
                            SSH_FORMAT_CHAR expects uint; caused a
                            rather nasty bug during development (I
                            used SshUInt8, which wasn't long
                            enough => ssh_decode_array blew the
                            stack).*/
  char *service_str = NULL, *hostbased_str = NULL;
  char *recv_pubkey_alg = NULL, *recv_hostname = NULL;
  char *recv_username = NULL;
  unsigned char *recv_pubkeyblob = NULL;
  size_t recv_pubkeyblob_len;
  /* Dug up by us. */
  char *pubkey_alg = NULL, *hostname = NULL, *username = NULL;
  unsigned char *pubkeyblob = NULL;
  char *used_service;
  size_t pubkeyblob_len;
  size_t hostname_len;

  /* Internal stuff*/
  char *hostkeyfile = NULL;
  
  SSH_FSM_GDATA(SshSigner);

  SSH_PRECOND(gdata->packet_payload);  

  /* If check is failed, go to this state. */
  SSH_FSM_SET_NEXT("signer_send_error");

  if (ssh_decode_array(gdata->packet_payload, gdata->packet_payload_len,
                       /* session id */
                       SSH_FORMAT_UINT32_STR, NULL, NULL,
                       /* SSH_MSG_USERAUTH_REQUEST (must be checked)*/
                       SSH_FORMAT_CHAR, &msg_byte,
                       /* user name (on remote host) */
                       SSH_FORMAT_UINT32_STR, NULL, NULL,
                       /* service (must be checked)*/
                       SSH_FORMAT_UINT32_STR, &service_str, NULL,
                       /* "hostbased" (must be checked)*/
                       SSH_FORMAT_UINT32_STR, &hostbased_str, NULL,
                       /* public key algorithm for hostkey (must
                          be checked)*/
                       SSH_FORMAT_UINT32_STR, &recv_pubkey_alg, NULL,
                       /* public hostkey and certificates (must be
                          checked)*/
                       SSH_FORMAT_UINT32_STR, &recv_pubkeyblob,
                       &recv_pubkeyblob_len,
                       /* client host name (must be checked)*/
                       SSH_FORMAT_UINT32_STR, &recv_hostname, NULL,
                       /* user name on client host (must be checked) */
                       SSH_FORMAT_UINT32_STR, &recv_username, NULL,
                       SSH_FORMAT_END) != gdata->packet_payload_len ||
      gdata->packet_payload_len == 0)
    {
      /* There was an error. */
      SSH_TRACE(0, ("Invalid packet."));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_GENERIC;
      gdata->error_message_to_ssh2 = ssh_xstrdup("Decoding supplied packet "
                                                 "failed.");
      goto error;
    }

  /* Get pubkeyblob, pubkeyblob_len, pubkey_alg, hostname and
     username. */

  /* Dig up hosts publickey. */
  if(gdata->serv_config->public_host_key_file[0] != '/')
    {
      ssh_dsprintf(&hostkeyfile, "%s/%s", SSH_SERVER_DIR,
                   gdata->serv_config->public_host_key_file);
    }
  else
    {
      hostkeyfile = ssh_xstrdup(gdata->serv_config->public_host_key_file);
    }

  SSH_TRACE(2, ("place to look for public key: %s", hostkeyfile));

  /* This pubkey*-stuff is for the client _host's_ public
     hostkey. */
  /* Getting pubkeyblob, pubkeyblob_len */
  SSH_DEBUG(4, ("Reading pubkey-blob from %s...", hostkeyfile));
  if (ssh2_key_blob_read(gdata->effective_user_data, hostkeyfile, TRUE,
                         NULL,
                         &pubkeyblob,
                         &pubkeyblob_len, NULL)
      != SSH_KEY_MAGIC_PUBLIC)
    {
      SSH_TRACE(1, ("Reading public key failed."));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_GENERIC;
      ssh_dsprintf(&gdata->error_message_to_ssh2, "Error reading public host "
                   "key from '%s'.", hostkeyfile);
      goto error;
    }

  SSH_DEBUG(4, ("done."));

  if ((pubkey_alg =
       ssh_pubkeyblob_type(pubkeyblob, pubkeyblob_len))
      == NULL)
    {
      SSH_TRACE(1, ("Couldn't figure out public key algorithm."));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_GENERIC;
      gdata->error_message_to_ssh2 =
        ssh_xstrdup("Couldn't determine public key algorithm from "
                    "public key.");
      goto error;
    }

  /* Getting hostname. */
  hostname = ssh_xmalloc(MAXHOSTNAMELEN*2);
  
  ssh_tcp_get_host_name(hostname, MAXHOSTNAMELEN*2);
  hostname_len = strlen(hostname);
  /* Sanity check */
  SSH_ASSERT(hostname_len + 2 < MAXHOSTNAMELEN);
  if (!strchr(hostname, '.'))
    {
      char *temp_name;
      if (!gdata->global_config->default_domain)
        {
          SSH_TRACE(2, ("hostname doesn't contain '.' and DefaultDomain " \
                        "isn't set. Hostbased authentication will "       \
                        "very likely fail."));
        }
      else
        {
          ssh_dsprintf(&temp_name, "%s.%s", hostname,
                       gdata->global_config->default_domain);
          ssh_xfree(hostname);
          hostname = temp_name;
          hostname_len = strlen(hostname);
        }
    }

  /* We want FQDN. */
  hostname[hostname_len] = '.';
  hostname[hostname_len + 1] = '\0';

  /* Getting username. */
  username = ssh_xstrdup(ssh_user_name(gdata->real_user));

  /* Check all parameters. */
  if (msg_byte != SSH_MSG_USERAUTH_REQUEST)
    {
      SSH_TRACE(1, ("Invalid packet."));
      SSH_DEBUG(1, ("msg_byte != SSH_MSG_USERAUTH_REQUEST " \
                    "(msg_byte = %d)", msg_byte));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
      gdata->error_message_to_ssh2 =
        ssh_xstrdup("Supplied packet is not a SSH_MSG_USERAUTH_REQUEST "
                    "packet.");
      goto error;
    }
  if (gdata->compat->hostbased_requested_service_draft_incompat)
    used_service = SSH_USERAUTH_SERVICE;
  else
    used_service = SSH_CONNECTION_SERVICE;
  
  if (strcmp(service_str, used_service) != 0)
    {
      SSH_TRACE(1, ("Invalid packet."));
      SSH_DEBUG(1, ("service_str != \"%s\" (it was '%s')", \
                    used_service, service_str));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
      ssh_dsprintf(&gdata->error_message_to_ssh2, "Supplied packet is not "
                   "used to request %s", used_service);
      goto error;
    }
  if (strcmp(hostbased_str, SSH_AUTH_HOSTBASED) != 0)
    {
      SSH_TRACE(1, ("Invalid packet."));
      SSH_DEBUG(1, ("hostbased_str != \"hostbased\" (it was '%s')", \
                    hostbased_str));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
      gdata->error_message_to_ssh2 =
        ssh_xstrdup("Supplied packet is not for \"hostbased\" authentication.");
      goto error;
    }
  /* XXX has to be changed when adding support for multiple hostkeys */
  if (strcmp(recv_pubkey_alg, pubkey_alg) != 0)
    {
      SSH_TRACE(1, ("Invalid packet."));
      SSH_DEBUG(1, ("Client gave us invalid pubkey-algorithms for our " \
                    "hostkey."));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
      gdata->error_message_to_ssh2 =
        ssh_xstrdup("Supplied packet contains wrong public key algorithm(s).");
      goto error;
    }

  gdata->pubkeytype = pubkey_alg;
  
  if (recv_pubkeyblob_len == pubkeyblob_len)
    {
      if (memcmp(recv_pubkeyblob, pubkeyblob, pubkeyblob_len) != 0)
        {
          SSH_TRACE(1, ("Invalid packet."));
          SSH_DEBUG(1, ("client gave us wrong (or corrupted) " \
                        "public key."));
#ifdef HEXDUMPS
          SSH_DEBUG_HEXDUMP(3, ("client gave us:"), \
                            recv_pubkeyblob, pubkeyblob_len);
          SSH_DEBUG_HEXDUMP(3, ("our pubkey:"), \
                            recv_pubkeyblob, pubkeyblob_len);
#endif /* HEXDUMPS */
          gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
          gdata->error_message_to_ssh2 =
            ssh_xstrdup("Supplied packet contains malformed public key.");
          goto error;
        }
    }
  else
    {
      SSH_TRACE(1, ("Invalid packet."));
      SSH_DEBUG(1, ("Client gave us wrong (or corrupted) public key. " \
                    "Lengths differ (received: %d ; ours: %d)", \
                    recv_pubkeyblob_len, pubkeyblob_len));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
      gdata->error_message_to_ssh2 =
        ssh_xstrdup("Supplied packet contains malformed public key.");
      goto error;
    }

  if (strcmp(recv_hostname, hostname) != 0)
    {
      SSH_TRACE(1, ("Invalid packet."));
      SSH_DEBUG(1, ("Wethinks the client gave us the wrong hostname. " \
                    "(client's opinion: '%s' ours: '%s'", \
                    recv_hostname, hostname));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
      ssh_dsprintf(&gdata->error_message_to_ssh2, "Supplied packet had the "
                   "wrong hostname (packet contained \"%s\", ssh-signer "
                   "got \"%s\").", recv_hostname, hostname);
      goto error;
    }
  if (strcmp(recv_username, username) != 0)
    {
      SSH_TRACE(1, ("Invalid packet."));
      SSH_DEBUG(1, ("Client definitely gave us the wrong user name. " \
                    "(it says: '%s' we know: '%s')", recv_username, \
                    username));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_DATA_VERIFICATION_FAILED;
      ssh_dsprintf(&gdata->error_message_to_ssh2, "Supplied packet had the "
                   "wrong client username (packet contained \"%s\", ssh-signer "
                   "got \"%s\").", recv_username, username);
      goto error;
    }

  /* Success. */
  SSH_TRACE(0, ("Received packet was OK."));
  SSH_FSM_SET_NEXT("signer_sign_hostbased_packet");
 error:
  ssh_xfree(service_str);
  ssh_xfree(hostbased_str);
  ssh_xfree(recv_pubkey_alg);
  ssh_xfree(recv_hostname);
  ssh_xfree(recv_username);
  ssh_xfree(recv_pubkeyblob);
  ssh_xfree(hostname);
  ssh_xfree(username);
  ssh_xfree(pubkeyblob);
  ssh_xfree(hostkeyfile);
  return SSH_FSM_CONTINUE;
}

void signer_signature_cb(SshCryptoStatus result,
                         const unsigned char *signature_buffer,
                         size_t sig_len,
                         void *context)
{
  SshFSMThread thread = (SshFSMThread)context;
  SSH_FSM_GDATA(SshSigner);
  SSH_PRECOND(gdata);

  gdata->op_handle = NULL;
  
  if (result != SSH_CRYPTO_OK)
    {
      SSH_TRACE(0, ("ssh_private_key_sign() returned %d.", result));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_SIGNING_OPERATION_FAILED;
      gdata->error_message_to_ssh2 = ssh_xstrdup("Signature operation failed.");
      goto error;
    }

#ifdef HEXDUMPS
  SSH_DEBUG_HEXDUMP(5, ("Signature:"), signature_buffer, sig_len);
#endif /* HEXDUMPS */
  /* Send it to client. */
  gdata->packet_type = SSH_AUTH_HOSTBASED_SIGNATURE;

  if (!gdata->compat->signature_encode_draft_incompat)
    {
      gdata->packet_payload_len =
        ssh_encode_array_alloc(&gdata->packet_payload,
                               SSH_FORMAT_UINT32_STR,
                               gdata->pubkeytype, strlen(gdata->pubkeytype),
                               SSH_FORMAT_UINT32_STR,
                               signature_buffer,
                               sig_len,
                               SSH_FORMAT_END);      
    }
  else
    {
      gdata->packet_payload_len =
        ssh_encode_array_alloc(&gdata->packet_payload,
                               SSH_FORMAT_DATA,
                               signature_buffer,
                               sig_len,
                               SSH_FORMAT_END);                         
    }
  
  SSH_FSM_SET_NEXT("signer_send_signature");
 error:
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

/* Sign the packet and send it to client. */
SSH_FSM_STEP(signer_sign_hostbased_packet)
{
  SSH_FSM_GDATA(SshSigner);
  char *comment = NULL;
  size_t sig_len;
  char *hostkeyfile = NULL;
  SshPrivateKey privkey;
  
  /* If signature op fails, go to this state. */
  SSH_FSM_SET_NEXT("signer_send_error");

  /* If we've gotten this far, the packet is ok, and it can be
     signed. */

  if(gdata->serv_config->public_host_key_file[0] != '/')
    {
      ssh_dsprintf(&hostkeyfile, "%s/%s", SSH_SERVER_DIR,
                   gdata->serv_config->host_key_file);
    }
  else
    {
      hostkeyfile = ssh_xstrdup(gdata->serv_config->host_key_file);
    }

  SSH_TRACE(2, ("place to look for private key: %s", hostkeyfile));

  if ((privkey = ssh_privkey_read(gdata->effective_user_data, hostkeyfile, "",
                                  &comment, NULL)) == NULL)
    {
      SSH_TRACE(0, ("ssh_privkey_read from %s failed.", hostkeyfile));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_SIGNING_OPERATION_FAILED;
      gdata->error_message_to_ssh2 = ssh_xstrdup("Couldn't read host key.");
      goto error;
    }
  

  /* Check how big a chunk our private key can sign (this is
     purely a sanity check, as both of our signature schemas do
     their own hashing) */
  sig_len = ssh_private_key_max_signature_input_len(privkey);

  SSH_TRACE(2, ("max input length for signing: %d", sig_len));

  if (sig_len == 0)
    {
      SSH_TRACE(0, ("private key not capable of signing! " \
                    "(definitely an error)"));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_SIGNING_OPERATION_FAILED;
      gdata->error_message_to_ssh2 = ssh_xstrdup("Private host key not capable "
                                                 "of signing.");
      goto error;
    }
  else if (sig_len != -1 && sig_len < gdata->packet_payload_len)
    {
      SSH_TRACE(0, ("private key can't sign our data. (too much " \
                    "data (data_len %d, max input len for signing " \
                    "%d))", gdata->packet_payload_len, sig_len));
      gdata->error_code_to_ssh2 = SIGNER_ERROR_SIGNING_OPERATION_FAILED;
      gdata->error_message_to_ssh2 = ssh_xstrdup("Private host key can't sign "
                                                 "packet (too much data).");
      goto error;
    }

  /* Do the actual signing. */

#ifdef HEXDUMPS
  SSH_DEBUG_HEXDUMP(5, ("Signing following data"),
                    gdata->packet_payload + 4, gdata->packet_payload_len - 4);
#endif /* HEXDUMPS */

  ssh_xfree(hostkeyfile);
  ssh_xfree(comment);

  SSH_FSM_ASYNC_CALL((gdata->op_handle =
                      ssh_private_key_sign_async(privkey,
                                                 gdata->packet_payload,
                                                 gdata->packet_payload_len,
                                                 signer_signature_cb,
                                                 thread)));
 error:
  ssh_xfree(hostkeyfile);
  ssh_xfree(comment);
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(signer_process_compat_flags)
{
  SSH_FSM_GDATA(SshSigner);
  size_t len = 0;
  char *compat_flags = NULL;
  
  SSH_PRECOND(gdata->packet_payload);
  SSH_PRECOND(gdata->packet_payload_len);
  
  len = ssh_decode_array(gdata->packet_payload, gdata->packet_payload_len,
                         SSH_FORMAT_UINT32_STR, &compat_flags, NULL,
                         SSH_FORMAT_END);

  if (len == 0 || len != gdata->packet_payload_len)
    {
      /* XXX Malformed packet. */
      ssh_xfree(compat_flags);
      gdata->error_code_to_ssh2 = SIGNER_ERROR_GENERIC;
      gdata->error_message_to_ssh2 =
        ssh_xstrdup("Malformed SSH_AUTH_HOSTBASED_COMPAT packet.");
      SSH_FSM_SET_NEXT("signer_error");
      return SSH_FSM_CONTINUE;
    }

  /* Do the actual processing. */
  {
    char *rest, *current;

    rest = compat_flags;
    
    while (strlen(rest) > 0 &&
           (current = ssh_app_param_list_get_next(rest)) != NULL)
      {
        rest += strlen(current);
        if (*rest == ',')
          rest++;
        
        if (strcmp(current, HOSTBASED_REQUESTED_SERVICE_DRAFT_INCOMPAT) == 0)
          gdata->compat->hostbased_requested_service_draft_incompat = TRUE;
        else if (strcmp(current, SIGNATURE_ENCODE_DRAFT_INCOMPAT) == 0)
          gdata->compat->signature_encode_draft_incompat = TRUE;
        /* If more compat-flags are needed, add them here. */
        ssh_xfree(current);
      }
  }

  ssh_xfree(compat_flags);

  /* We are now prepared to receive more packets. */
  ssh_packet_wrapper_can_receive(gdata->wrapper, TRUE);
  
  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(signer_send_signature)
{
  SSH_FSM_GDATA(SshSigner);

  SSH_FSM_SET_NEXT("signer_suspend");
  
  gdata->packet_pending = TRUE;
  
  if (ssh_packet_wrapper_can_send(gdata->wrapper))
    ssh_register_timeout(0L, 0L, signer_can_send, gdata);
  
  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(signer_finish)
{
  return SSH_FSM_FINISH;
}

SshFSMStateMapItem signer_states[] =
{
  { "signer_suspend", "Suspend thread", signer_suspend },
  
  { "signer_process_compat_flags", "Process compatibility flags",
    signer_process_compat_flags },
  { "signer_process_error_packet", "Process received error packet",
    signer_process_error_packet },  
  { "signer_process_end", "Process received END packet",
    signer_process_end },
  { "signer_send_error", "Send error packet", signer_send_error },
  
  { "signer_check_hostbased_packet",
    "Check hostbased-authentication packet validity",
    signer_check_hostbased_packet },
  { "signer_sign_hostbased_packet", "Sign the hostbased packet",
    signer_sign_hostbased_packet },
  { "signer_send_signature", "Send the signature to the application",
    signer_send_signature },  

  { "signer_finish", "We're ready", signer_finish }
};

void signer_received_packet(SshPacketType type,
                            const unsigned char *data, size_t len,
                            void *context)
{
  SshSigner signer = (SshSigner) context;

  SSH_DEBUG(2, ("Received packet, length = %ld", len));
#ifdef HEXDUMPS
  SSH_DEBUG_HEXDUMP(3, ("packet:"), data, len);
#endif /* HEXDUMPS */

  ssh_packet_wrapper_can_receive(signer->wrapper, FALSE);

  SSH_DEBUG(3, ("Previous state: %d", signer->state));
  
  switch (type)
    {
    case SSH_AUTH_HOSTBASED_PACKET:
      if (signer->state != SIGNER_START &&
          signer->state != SIGNER_COMPAT_FLAGS_RECEIVED)
        {
          /* This must be sent first or after compat flags. */
          /* XXX send error */
          SSH_NOTREACHED;
        }
        
      signer->state = SIGNER_HOSTBASED_PACKET_RECEIVED;
      ssh_fsm_set_next(signer->main_thread, "signer_check_hostbased_packet");
      break;
    case SSH_AUTH_HOSTBASED_END:
      if (signer->state == SIGNER_DEAD)
        {
          /* This must be sent only once. */
          /* XXX process error. */
          SSH_NOTREACHED;
        }
      signer->state = SIGNER_DEAD;
      ssh_fsm_set_next(signer->main_thread, "signer_process_end");
      break;
    case SSH_AUTH_HOSTBASED_ERROR:
      if (signer->state == SIGNER_DEAD)
        {
          /* This must be sent oly once. */
          /* XXX process error. */
          SSH_NOTREACHED;
        }
      signer->state = SIGNER_DEAD;
      ssh_fsm_set_next(signer->main_thread, "signer_process_error");
      break;
    case SSH_AUTH_HOSTBASED_COMPAT:
      if (signer->state != SIGNER_START)
        {
          /* This has to be sent first, if at all. */
          /* XXX send error */
          SSH_NOTREACHED;
        }
      signer->state = SIGNER_COMPAT_FLAGS_RECEIVED;
      ssh_fsm_set_next(signer->main_thread, "signer_process_compat_flags");
      break;
    default:
      SSH_TRACE(0, ("Invalid packet type %d received from ssh2-client.",
                    type));
      /* XXX */
      SSH_NOTREACHED;
    }

  ssh_fsm_continue(signer->main_thread);

  if (signer->packet_payload != NULL)
    ssh_xfree(signer->packet_payload);

  signer->packet_payload = ssh_xmemdup(data, len);  
  signer->packet_payload_len = len;
}

void signer_received_eof(void *context)
{
  SshSigner signer = (SshSigner) context;

  SSH_DEBUG(3, ("Received EOF from packetstream."));
  
  if (signer->state != SIGNER_DEAD)
    {
      ssh_warning("Received EOF from packetstream before transactions were "
                  "complete.");
      signer->state = SIGNER_DEAD;
    }
  
  ssh_fsm_set_next(signer->main_thread, "signer_finish");
  ssh_fsm_continue(signer->main_thread);
}

void signer_can_send(void *context)
{
  SshSigner signer = (SshSigner)context;

  if (!signer->packet_pending)
    return;

  signer->packet_pending = FALSE;
  
  SSH_PRECOND(signer);
  SSH_PRECOND(signer->packet_type);
  SSH_PRECOND(signer->packet_payload);
  SSH_PRECOND(signer->packet_payload_len);
    
  ssh_packet_wrapper_send(signer->wrapper,
                          signer->packet_type,
                          signer->packet_payload,
                          signer->packet_payload_len);

  signer->packet_type = (SshPacketType)0;
  ssh_xfree(signer->packet_payload);
  signer->packet_payload = NULL;
  signer->packet_payload_len = 0L;

  ssh_packet_wrapper_can_receive(signer->wrapper, TRUE);
  
  ssh_fsm_continue(signer->main_thread);  
}


void signer_ssh_fatal(const char *message, void *context)
{
  fprintf(stderr, "%s:FATAL:%s\n", progname, message);
  fflush(stderr);
}

void signer_ssh_warning(const char *message, void *context)
{
  SshSigner signer = (SshSigner) context;

  if (!signer->quiet)
    fprintf(stderr, "%s:%s\n", progname, message);
  fflush(stderr);
}

void signer_ssh_debug(const char *message, void *context)
{
  SshSigner signer = (SshSigner) context;

  if (!signer->quiet)
    fprintf(stderr, "%s:%s\n", progname, message);
  fflush(stderr);
}

void signer_destructor(void *gdata)
{
  SshSigner signer = (SshSigner) gdata;
  SSH_PRECOND(signer);

  SSH_DEBUG(3, ("Destroying gdata..."));
  if (signer->op_handle)
    {
      ssh_operation_abort(signer->op_handle);
      signer->op_handle = NULL;
    }
  ssh_packet_wrapper_destroy(signer->wrapper);
  ssh_xfree(signer->packet_payload);
  ssh_config_free(signer->serv_config);
  ssh_config_free(signer->global_config);
  ssh_user_free(signer->effective_user_data, FALSE);
  ssh_user_free(signer->real_user, FALSE);
  ssh_xfree(signer->compat);
  ssh_xfree(signer->error_message_to_ssh2);
  ssh_xfree(signer->pubkeytype);
  SSH_DEBUG(3, ("done."));
}

void signer_thread_destructor(void *tdata)
{
  /* Nothing here yet! */
  return;
}

void signer_init_context(SshSigner signer)
{
  memset(signer, 0, sizeof(*signer));
  
  signer->compat = ssh_xcalloc(1, sizeof(*signer->compat));
}

int main(int argc, char **argv)
{
  SshStream stdio_stream;
  SshSigner signer;
  SshFSM fsm;  
  char *config_filename;
  char *temp_name;
  
#ifdef SLEEP_AFTER_STARTUP
  sleep(30);
#endif /* SLEEP_AFTER_STARTUP */

  /* Get program name (without path). */
  if ((temp_name = strrchr(argv[0], '/')) != NULL)
    progname = ssh_xstrdup(temp_name + 1);
  else
    progname = ssh_xstrdup(argv[0]);

  /* XXX there should be a way to give command-line parameters to this
     program, but, they should only be used if the uid is the same as
     euid. */
  ssh_event_loop_initialize();

  fsm = ssh_fsm_allocate(sizeof(*signer),
                         signer_states,
                         SSH_FSM_NUM_STATES(signer_states),
                         signer_destructor);

  signer = ssh_fsm_get_gdata_fsm(fsm);

  signer_init_context(signer);
  
  signer->fsm = fsm;
  
#ifdef SIGNER_QUIET
  signer->quiet = TRUE;
#else /* SIGNER_QUIET */
  signer->quiet = FALSE;
#endif /* SIGNER_QUIET */
  ssh_debug_register_callbacks(signer_ssh_fatal, signer_ssh_warning,
                               signer_ssh_debug, (void *)signer);
#ifdef SIGNER_DEBUG
  ssh_debug_set_global_level(5);
#endif /* SIGNER_DEBUG */

  /* Act as server. */
  signer->serv_config = ssh_server_create_config();
 
  signer->global_config = ssh_client_create_config();
  /* Initialize user context with euid. This is used to dig up the
     hostkey and such. */
  signer->effective_user_data = ssh_user_initialize_with_uid(geteuid(), FALSE);

  signer->real_user = ssh_user_initialize(NULL, FALSE);

  ssh_randseed_open(signer->effective_user_data,
                    signer->serv_config);

  /* XXX what about alternative config files? This should be possible
     to configure somehow. An option for configure is probably a good
     idea. */
  ssh_dsprintf(&config_filename, "%s/%s",
               SSH_SERVER_DIR, SSH_SERVER_CONFIG_FILE);
  
  if (!ssh_config_read_file(signer->effective_user_data, signer->serv_config,
                            NULL, config_filename, NULL))
    ssh_warning("%s: Failed to read config file %s", argv[0],
                config_filename);

  ssh_xfree(config_filename);

  ssh_userfile_init(ssh_user_name(signer->real_user),
                    ssh_user_uid(signer->real_user),
                    ssh_user_gid(signer->real_user), NULL, NULL);
  
  if (!ssh_config_read_file(signer->real_user, signer->global_config,
                            NULL, SSH_CLIENT_GLOBAL_CONFIG_FILE, NULL))
    ssh_warning("%s: Failed to read config file %s", argv[0],
                SSH_CLIENT_GLOBAL_CONFIG_FILE);

  SSH_TRACE(2, ("public key file: %s", signer->serv_config->public_host_key_file));
  SSH_TRACE(2, ("private key file: %s", signer->serv_config->host_key_file));
  SSH_TRACE(2, ("randomseed file: %s", signer->serv_config->random_seed_file));

  stdio_stream = ssh_stream_fd_wrap2(fileno(stdin), fileno(stdout),
                                     TRUE);

  signer->wrapper = ssh_packet_wrap(stdio_stream,
                                    signer_received_packet,
                                    signer_received_eof,
                                    signer_can_send,
                                    signer);

  signer->main_thread = ssh_fsm_spawn(signer->fsm,
                                      0,
                                      "signer_suspend",
                                      NULL,
                                      signer_thread_destructor);
  ssh_fsm_set_thread_name(signer->main_thread, "main_thread");
  
  ssh_packet_wrapper_can_receive(signer->wrapper, TRUE);
  
  ssh_event_loop_run();

  ssh_event_loop_uninitialize();
  
  return 0;
}

