openssh-rust / openssh

Scriptable SSH through OpenSSH in Rust
Apache License 2.0
232 stars 37 forks source link

how to use Username and password authetication #119

Closed baoyachi closed 10 months ago

baoyachi commented 1 year ago

Is there have an example of connect on SFTP username and password?

NobodyXu commented 1 year ago

openssh-sftp-client only provides the sftp client and the ssh client is provided by openssh.

I will transfer this issue to openssh

NobodyXu commented 1 year ago

Transfer is done.

For openssh, the only way to use password authentication is to create the ssh multiplex master yourself and asks password from the user, then use Session::resume or Session::resume_mux to create a openssh::Session.

Then you can spawn sftp session by simply calling Sftp::from_session on the Session you get.

baoyachi commented 1 year ago

Thx,I'm try it

baoyachi commented 1 year ago

Casued error: "kex_exchange_identification: connection closed by remote host"

NobodyXu commented 1 year ago

@baoyachi Can you show me the code so that I can help you debug the problem?

baoyachi commented 1 year ago

I will send PR later to solve this issue.

baoyachi commented 1 year ago

@NobodyXu Currently, openssh supports username, and the password can be seen here. If possible, I can submit a PR.


openssh-extension: Based on openssh extension。

Currently, openssh only supports public key authentication methods. However, in certain specific scenarios, it may still be necessary to use user password for sftp login.

This repository is based on Openssh and extends support for sftp user password authentication.

NobodyXu commented 1 year ago

@baoyachi I'm willing to accept a PR for this.

Regarding the opendal, isn't there already official support for sftp using openssh + openssh_sftp_client?

baoyachi commented 1 year ago

Yes, currently opendal does not support SFTP username and password login. Due to different dependencies, I have implemented compatibility. If this PR is merged by openssh, openssh-sftp-client,the latest version of openssh, openssh-sftp-client feature can be updated in the future of opendal

NobodyXu commented 1 year ago

Thanks for explanation, I am willing to accpet PR for password login.

NobodyXu commented 1 year ago

@baoyachi Reading your implementation for password login, I do found an issue: The password is passed via commandline.

This means that it can be discovered by looking at /proc/<pid>/cmdline, which is very problematic.

I do find a solution that is called sshpass which workarounds this by emulating a tty and pass the password.

I think we could do the same in openssh here.

NobodyXu commented 1 year ago

sshpass can be downloaded from here

NobodyXu commented 1 year ago

Here's its main.c:

```C /* This file is part of "sshpass", a tool for batch running password ssh authentication * Copyright (C) 2006 Lingnu Open Source Consulting Ltd. * Copyright (C) 2015-2016, 2021 Shachar Shemesh * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version, provided that it was accepted by * Lingnu Open Source Consulting Ltd. as an acceptable license for its * projects. Consult http://www.lingnu.com/licenses.html * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #if HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #if HAVE_TERMIOS_H #include #endif #include #include #include #include enum program_return_codes { RETURN_NOERROR, RETURN_INVALID_ARGUMENTS, RETURN_CONFLICTING_ARGUMENTS, RETURN_RUNTIME_ERROR, RETURN_PARSE_ERRROR, RETURN_INCORRECT_PASSWORD, RETURN_HOST_KEY_UNKNOWN, RETURN_HOST_KEY_CHANGED, RETURN_HELP, }; // Some systems don't define posix_openpt #ifndef HAVE_POSIX_OPENPT int posix_openpt(int flags) { return open("/dev/ptmx", flags); } #endif #define DEFAULT_ENV_PASSWORD "SSHPASS" int runprogram( int argc, char *argv[] ); void reliable_write( int fd, const void *data, size_t size ); int handleoutput( int fd ); void window_resize_handler(int signum); void sigchld_handler(int signum); void term_handler(int signum); void term_child(int signum); int match( const char *reference, const char *buffer, ssize_t bufsize, int state ); void write_pass( int fd ); struct { enum { PWT_STDIN, PWT_FILE, PWT_FD, PWT_PASS } pwtype; union { const char *filename; int fd; const char *password; } pwsrc; const char *pwprompt; int verbose; char *orig_password; } args; static void hide_password() { assert(args.pwsrc.password==NULL); args.pwsrc.password = strdup(args.orig_password); // Hide the original password from prying eyes while( *args.orig_password != '\0' ) { *args.orig_password = '\0'; ++args.orig_password; } args.orig_password = NULL; } static void show_help() { printf("Usage: " PACKAGE_NAME " [-f|-d|-p|-e[env_var]] [-hV] command parameters\n" " -f filename Take password to use from file.\n" " -d number Use number as file descriptor for getting password.\n" " -p password Provide password as argument (security unwise).\n" " -e[env_var] Password is passed as env-var \"env_var\" if given, \"SSHPASS\" otherwise.\n" " With no parameters - password will be taken from stdin.\n\n" " -P prompt Which string should sshpass search for to detect a password prompt.\n" " -v Be verbose about what you're doing.\n" " -h Show help (this screen).\n" " -V Print version information.\n" "At most one of -f, -d, -p or -e should be used.\n"); } // Parse the command line. Fill in the "args" global struct with the results. Return argv offset // on success, and a negative number on failure static int parse_options( int argc, char *argv[] ) { int error=-1; int opt; // Set the default password source to stdin args.pwtype=PWT_STDIN; args.pwsrc.fd=0; #define VIRGIN_PWTYPE if( args.pwtype!=PWT_STDIN ) { \ fprintf(stderr, "Conflicting password source\n"); \ error=RETURN_CONFLICTING_ARGUMENTS; } while( (opt=getopt(argc, argv, "+f:d:p:P:he::Vv"))!=-1 && error==-1 ) { switch( opt ) { case 'f': // Password should come from a file VIRGIN_PWTYPE; args.pwtype=PWT_FILE; args.pwsrc.filename=optarg; break; case 'd': // Password should come from an open file descriptor VIRGIN_PWTYPE; args.pwtype=PWT_FD; args.pwsrc.fd=atoi(optarg); break; case 'p': // Password is given on the command line VIRGIN_PWTYPE; args.pwtype=PWT_PASS; args.orig_password=optarg; break; case 'P': args.pwprompt=optarg; break; case 'v': args.verbose++; break; case 'e': VIRGIN_PWTYPE; args.pwtype=PWT_PASS; if( optarg==NULL ) optarg = "SSHPASS"; args.orig_password=getenv(optarg); if( args.orig_password==NULL ) { fprintf(stderr, "sshpass: -e option given but \"%s\" environment variable is not set.\n", optarg); error=RETURN_INVALID_ARGUMENTS; } hide_password(); unsetenv(optarg); break; case '?': case ':': error=RETURN_INVALID_ARGUMENTS; break; case 'h': return -(RETURN_HELP+1); case 'V': printf("%s\n" "(C) 2006-2011 Lingnu Open Source Consulting Ltd.\n" "(C) 2015-2016, 2021-2022 Shachar Shemesh\n" "This program is free software, and can be distributed under the terms of the GPL\n" "See the COPYING file for more information.\n" "\n" "Using \"%s\" as the default password prompt indicator.\n", PACKAGE_STRING, PASSWORD_PROMPT ); exit(0); break; } } if( error>=0 ) return -(error+1); else return optind; } int main( int argc, char *argv[] ) { int opt_offset=parse_options( argc, argv ); if( opt_offset==-(RETURN_HELP+1) ) { show_help(); return 0; } if( opt_offset<0 ) { // There was some error fprintf(stderr, "Use \"sshpass -h\" to get help\n"); return -(opt_offset+1); // -1 becomes 0, -2 becomes 1 etc. } if( argc-opt_offset<1 ) { show_help(); return 0; } if( args.orig_password!=NULL ) { hide_password(); } return runprogram( argc-opt_offset, argv+opt_offset ); } /* Global variables so that this information be shared with the signal handler */ static int ourtty; // Our own tty static int masterpt; int childpid; int termsig; int runprogram( int argc, char *argv[] ) { struct winsize ttysize; // The size of our tty // We need to interrupt a select with a SIGCHLD. In order to do so, we need a SIGCHLD handler signal( SIGCHLD, sigchld_handler ); // Create a pseudo terminal for our process masterpt=posix_openpt(O_RDWR); if( masterpt==-1 ) { perror("Failed to get a pseudo terminal"); return RETURN_RUNTIME_ERROR; } fcntl(masterpt, F_SETFL, O_NONBLOCK); if( grantpt( masterpt )!=0 ) { perror("Failed to change pseudo terminal's permission"); return RETURN_RUNTIME_ERROR; } if( unlockpt( masterpt )!=0 ) { perror("Failed to unlock pseudo terminal"); return RETURN_RUNTIME_ERROR; } ourtty=open("/dev/tty", 0); if( ourtty!=-1 && ioctl( ourtty, TIOCGWINSZ, &ttysize )==0 ) { signal(SIGWINCH, window_resize_handler); ioctl( masterpt, TIOCSWINSZ, &ttysize ); } const char *name=ptsname(masterpt); int slavept; /* Comment no. 3.14159 This comment documents the history of code. We need to open the slavept inside the child process, after "setsid", so that it becomes the controlling TTY for the process. We do not, otherwise, need the file descriptor open. The original approach was to close the fd immediately after, as it is no longer needed. It turns out that (at least) the Linux kernel considers a master ptty fd that has no open slave fds to be unused, and causes "select" to return with "error on fd". The subsequent read would fail, causing us to go into an infinite loop. This is a bug in the kernel, as the fact that a master ptty fd has no slaves is not a permenant problem. As long as processes exist that have the slave end as their controlling TTYs, new slave fds can be created by opening /dev/tty, which is exactly what ssh is, in fact, doing. Our attempt at solving this problem, then, was to have the child process not close its end of the slave ptty fd. We do, essentially, leak this fd, but this was a small price to pay. This worked great up until openssh version 5.6. Openssh version 5.6 looks at all of its open file descriptors, and closes any that it does not know what they are for. While entirely within its prerogative, this breaks our fix, causing sshpass to either hang, or do the infinite loop again. Our solution is to keep the slave end open in both parent AND child, at least until the handshake is complete, at which point we no longer need to monitor the TTY anyways. */ sigset_t sigmask, sigmask_select; // Set the signal mask during the select sigemptyset(&sigmask_select); // And during the regular run sigemptyset(&sigmask); sigaddset(&sigmask, SIGCHLD); sigaddset(&sigmask, SIGHUP); sigaddset(&sigmask, SIGTERM); sigaddset(&sigmask, SIGINT); sigaddset(&sigmask, SIGTSTP); sigprocmask( SIG_SETMASK, &sigmask, NULL ); signal(SIGHUP, term_handler); signal(SIGTERM, term_handler); signal(SIGINT, term_handler); signal(SIGTSTP, term_handler); childpid=fork(); if( childpid==0 ) { // Child // Re-enable all signals to child sigprocmask( SIG_SETMASK, &sigmask_select, NULL ); // Detach us from the current TTY setsid(); // Attach the process to a controlling TTY. slavept=open(name, O_RDWR | O_NOCTTY); // On some systems, an open(2) is insufficient to set the controlling tty (see the documentation for // TIOCSCTTY in tty(4)). if (ioctl(slavept, TIOCSCTTY, 0) == -1) { perror("sshpass: Failed to set controlling terminal in child (TIOCSCTTY)"); exit(RETURN_RUNTIME_ERROR); } close( slavept ); // We don't need the controlling TTY actually open close( masterpt ); char **new_argv=malloc(sizeof(char *)*(argc+1)); int i; for( i=0; i0 ) { if( FD_ISSET( masterpt, &readfd ) ) { int ret; if( (ret=handleoutput( masterpt )) ) { // Authentication failed or any other error // handleoutput returns positive error number in case of some error, and a negative value // if all that happened is that the slave end of the pt is closed. if( ret>0 ) { close( masterpt ); // Signal ssh that it's controlling TTY is now closed close(slavept); } terminate=ret; if( terminate ) { close( slavept ); } } } } wait_id=waitpid( childpid, &status, WNOHANG ); } else { wait_id=waitpid( childpid, &status, 0 ); } } while( wait_id==0 || (!WIFEXITED( status ) && !WIFSIGNALED( status )) ); if( terminate>0 ) return terminate; else if( WIFEXITED( status ) ) return WEXITSTATUS(status); else return 255; } int handleoutput( int fd ) { // We are looking for the string static int prevmatch=0; // If the "password" prompt is repeated, we have the wrong password. static int state1, state2, state3; static int firsttime = 1; static const char *compare1=PASSWORD_PROMPT; // Asking for a password static const char compare2[]="The authenticity of host "; // Asks to authenticate host static const char compare3[] = "differs from the key for the IP address"; // Key changes // static const char compare3[]="WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!"; // Warns about man in the middle attack // The remote identification changed error is sent to stderr, not the tty, so we do not handle it. // This is not a problem, as ssh exists immediately in such a case char buffer[256]; int ret=0; if( args.pwprompt ) { compare1 = args.pwprompt; } if( args.verbose && firsttime ) { firsttime=0; fprintf(stderr, "SSHPASS: searching for password prompt using match \"%s\"\n", compare1); } int numread=read(fd, buffer, sizeof(buffer)-1 ); buffer[numread] = '\0'; if( args.verbose ) { fprintf(stderr, "SSHPASS: read: %s\n", buffer); } state1=match( compare1, buffer, numread, state1 ); // Are we at a password prompt? if( compare1[state1]=='\0' ) { if( !prevmatch ) { if( args.verbose ) fprintf(stderr, "SSHPASS: detected prompt. Sending password.\n"); write_pass( fd ); state1=0; prevmatch=1; } else { // Wrong password - terminate with proper error code if( args.verbose ) fprintf(stderr, "SSHPASS: detected prompt, again. Wrong password. Terminating.\n"); ret=RETURN_INCORRECT_PASSWORD; } } if( ret==0 ) { state2=match( compare2, buffer, numread, state2 ); // Are we being prompted to authenticate the host? if( compare2[state2]=='\0' ) { if( args.verbose ) fprintf(stderr, "SSHPASS: detected host authentication prompt. Exiting.\n"); ret=RETURN_HOST_KEY_UNKNOWN; } else { state3 = match( compare3, buffer, numread, state3 ); // Host key changed if ( compare3[state3]=='\0' ) { ret=RETURN_HOST_KEY_CHANGED; } } } return ret; } int match( const char *reference, const char *buffer, ssize_t bufsize, int state ) { // This is a highly simplisic implementation. It's good enough for matching "Password: ", though. int i; for( i=0;reference[state]!='\0' && i0 ) { kill( childpid, signum ); } } } void reliable_write( int fd, const void *data, size_t size ) { ssize_t result = write( fd, data, size ); if( result!=size ) { if( result<0 ) { perror("SSHPASS: write failed"); } else { fprintf(stderr, "SSHPASS: Short write. Tried to write %lu, only wrote %ld\n", size, result); } } } ```
baoyachi commented 1 year ago

Can we proceed in two steps?

The first step is that OpenSSH supports username and password.

Step 2: Transform the sshpass function.

NobodyXu commented 1 year ago

@baoyachi Yes, I'm ok with the plan, but then we would have to add a warning to its doc first, and ideally I'd like step 2 to be done before next release is out.

baoyachi commented 1 year ago

@baoyachi Yes, I'm ok with the plan, but then we would have to add a warning to its doc first, and ideally I'd like step 2 to be done before next release is out.

I agree.

NobodyXu commented 1 year ago

@baoyachi I've found pty-process for creating pty for a subprocess and it can be used with tokio::proess.

sshpass implementation is pretty simple in principle: It reads from pty and search for "assword", once it is found the password will be written to the pty.

baoyachi commented 1 year ago

@NobodyXu pty-process build error on macos system.

NobodyXu commented 1 year ago

@NobodyXu pty-process build error on macos system.

Can you report an error to upstream please?

jonhoo commented 1 year ago

I recommend just declaring this feature as explicitly unsupported. It is generally less secure, and once you have to hook into things to provide passwords on STDIN, it becomes a hassle to make sure you don't keep the password in memory for too long, etc. Besides, turning a password-based SSH host into a pubkey one is a one-time:

ssh-keygen
ssh-copy-id <target>

and that feels much much better than trying to force in password-every-time.