From d1a7759bd17c2a90c580ec33974f3d3bc5096fde Mon Sep 17 00:00:00 2001 From: TJ Saunders Date: Mon, 18 Dec 2023 17:57:30 -0800 Subject: [PATCH] Finish adding integration tests, and the necessary OpenSSH key option parsing routines. --- keys.c | 239 ++++++--- .../test_rsa_key_with_option_command.pub | 1 + .../test_rsa_key_with_option_from.pub | 1 + ..._rsa_key_with_option_no_touch_required.pub | 1 + ...st_rsa_key_with_option_verify_required.pub | 1 + .../ProFTPD/Tests/Modules/mod_sftp_openssh.pm | 494 ++++++++++++++++++ 6 files changed, 672 insertions(+), 65 deletions(-) create mode 100644 t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_command.pub create mode 100644 t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_from.pub create mode 100644 t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_no_touch_required.pub create mode 100644 t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_verify_required.pub diff --git a/keys.c b/keys.c index 970f73d..ba0bfc6 100644 --- a/keys.c +++ b/keys.c @@ -28,6 +28,73 @@ static const char *trace_channel = "sftp.openssh"; +/* Return the index ("span") of the next character, assuming text that + * contains quoted sections. Make sure we handle the case where the + * character of interest is not found in the given text, by returning -1 + * in such cases. + */ +static int strnqspn(const char *text, size_t text_len, char c) { + int len = 0, quoted = FALSE; + + for (; *text && (quoted || (*text != ' ' && *text != '\t')); text++) { + if (text[0] == '\\' && + text[1] == '"') { + /* Skip past this escaped quote. */ + text++; + len += 2; + + } else { + if (text[0] == c && + quoted == FALSE) { + return len; + + } else if (text[0] == '"') { + quoted = !quoted; + } + + len++; + } + } + + /* If we reach here, we've reached the end of the text without finding + * any occurrences of the requested character. + */ + errno = ENOENT; + return -1; +} + +/* Get the span of an options field of text, which includes any quoted spaces. + * Returns the span length, or -1 on failure, such as for unterminated quotes. + */ +static int get_optspn(const char *text) { + int len = 0, quoted = FALSE; + + for (; *text && (quoted || (*text != ' ' && *text != '\t')); text++) { + if (text[0] == '\\' && + text[1] == '"') { + /* Skip past this escaped quote. */ + text++; + len += 2; + + } else { + if (text[0] == '"') { + quoted = !quoted; + } + + len++; + } + } + + if (*text == '\0' && + quoted == TRUE) { + /* We reached the end of text, and we're still quoted. */ + errno = EINVAL; + return -1; + } + + return len; +} + static int is_supported_key_type(const char *key_desc, size_t key_desclen) { int supported = FALSE; @@ -57,87 +124,85 @@ static int is_supported_key_type(const char *key_desc, size_t key_desclen) { return supported; } -/* Return count of handled/parsed options. */ -static unsigned int parse_options(pool *p, const char *text, - pr_table_t *headers) { - const char *ptr; - unsigned int count = 0; - - ptr = text; - while (*ptr && - !PR_ISSPACE(*ptr)) { - const char *opt = NULL; - size_t opt_len = 0; +struct key_opt { + const char *name; + const char *header_name; + const char *header_val; +}; - pr_signals_handle(); +static struct key_opt supported_opts[] = { +#if PROFTPD_VERSION_NUMBER >= 0x0001030901 + /* ProFTPD 1.3.9rc1 is when support for FIDO security keys first appeared. */ - /* There are currently only a few OpenSSH options of interest to us. */ + { "touch-required", SFTP_KEYSTORE_HEADER_FIDO_TOUCH_REQUIRED, "true" }, + { "no-touch-required", SFTP_KEYSTORE_HEADER_FIDO_TOUCH_REQUIRED, "false" }, -#if PROFTPD_VERSION_NUMBER >= 0x0001030901 - /* ProFTPD 1.3.9rc1 is when support for FIDO security keys first appeared. - */ - opt = "touch-required"; - opt_len = strlen(opt); - if (strncasecmp(ptr, opt, opt_len) == 0) { - count++; + { "verify-required", SFTP_KEYSTORE_HEADER_FIDO_VERIFY_REQUIRED, "true" }, + { "no-verify-required", SFTP_KEYSTORE_HEADER_FIDO_VERIFY_REQUIRED, "false" }, +#endif /* Prior to ProFTPD 1.3.9rc1 */ - if (pr_table_add_dup(headers, SFTP_KEYSTORE_HEADER_FIDO_TOUCH_REQUIRED, - "true", 0) < 0) { - pr_trace_msg(trace_channel, 19, - "error adding '%s' header from key: %s", - SFTP_KEYSTORE_HEADER_FIDO_TOUCH_REQUIRED, strerror(errno)); + { NULL, NULL, NULL } +}; - } else { - pr_trace_msg(trace_channel, 22, "added header: '%s: true' to notes", - SFTP_KEYSTORE_HEADER_FIDO_TOUCH_REQUIRED); - } +static int parse_key_option(pool *p, const char *text, size_t text_len, + pr_table_t *headers) { + register unsigned int i; + int res = -1; - ptr += opt_len; + for (i = 0; supported_opts[i].name != NULL; i++) { + size_t opt_len; - if (*ptr == ',') { - ptr++; - } + opt_len = strlen(supported_opts[i].name); + if (text_len < opt_len) { + continue; } - opt = "verify-required"; - opt_len = strlen(opt); - if (strncasecmp(ptr, opt, opt_len) == 0) { - count++; - - if (pr_table_add_dup(headers, SFTP_KEYSTORE_HEADER_FIDO_VERIFY_REQUIRED, - "true", 0) < 0) { + if (strncasecmp(text, supported_opts[i].name, opt_len) == 0) { + if (pr_table_add_dup(headers, supported_opts[i].header_name, + supported_opts[i].header_val, 0) < 0) { pr_trace_msg(trace_channel, 19, "error adding '%s' header from key: %s", - SFTP_KEYSTORE_HEADER_FIDO_VERIFY_REQUIRED, strerror(errno)); + supported_opts[i].header_name, strerror(errno)); } else { - pr_trace_msg(trace_channel, 22, "added header: '%s: true' to notes", - SFTP_KEYSTORE_HEADER_FIDO_VERIFY_REQUIRED); + pr_trace_msg(trace_channel, 22, "added header: '%s: %s' to notes", + supported_opts[i].header_name, supported_opts[i].header_val); } - ptr += opt_len; - - if (*ptr == ',') { - ptr++; - } + res = 0; } -#endif /* Prior to ProFTPD 1.3.9rc1 */ + } - if (*ptr == '\0' || - PR_ISSPACE(*ptr)) { - /* End of options. */ - break; - } + return res; +} - if (*ptr != ',') { - /* Unsupported option. */ - ptr++; - } +/* Return count of handled/parsed options. */ +static unsigned int parse_key_options(pool *p, const char *text, + size_t text_len, pr_table_t *headers) { + unsigned int count = 0; + int len; - if (*ptr == '\0') { - /* End of options. */ - break; + len = strnqspn(text, text_len, ','); + while (len > 0) { + pr_signals_handle(); + + if (parse_key_option(p, text, len, headers) == 0) { + count++; } + + text += len; + text_len -= len; + + /* Skip the comma, too. */ + text++; + text_len--; + + len = strnqspn(text, text_len, ','); + } + + /* Last option. */ + if (parse_key_option(p, text, text_len, headers) == 0) { + count++; } return count; @@ -225,7 +290,7 @@ int sftp_openssh_keys_parse(pool *p, const char *line, size_t linelen, const char **comment, pr_table_t *headers) { const char *key_opts = NULL, *ptr; int supported_key_type = FALSE; - size_t comment_len = 0, len; + size_t comment_len = 0, key_optslen = 0, len; ptr = line; @@ -244,11 +309,43 @@ int sftp_openssh_keys_parse(pool *p, const char *line, size_t linelen, /* field: options, or key type */ supported_key_type = is_supported_key_type(ptr, len); if (supported_key_type == FALSE) { - /* Assume we are dealing with key options; we'll parse these later. */ + int res; + + /* Assume we are dealing with key options; we'll parse these later. + * Since the option specifications can themselves have embedded, quoted + * spaces, we cannot use strcspn(3) directly here to determine the length + * of this options field. + */ + res = get_optspn(ptr); + if (res < 0) { + errno = EINVAL; + return -1; + } + key_opts = ptr; + key_optslen = (size_t) res; - /* XXX Skip options */ - /* XXX len = ... */ + pr_trace_msg(trace_channel, 22, "skipping options '%.*s'", + (int) key_optslen, key_opts); + + /* Advance past the options. */ + ptr += key_optslen; + linelen -= key_optslen; + + /* Skip whitespace. */ + for (; *ptr && PR_ISSPACE(*ptr); ptr++) { + linelen--; + } + + if (*ptr == '\0') { + errno = EINVAL; + return -1; + } + + len = strcspn(ptr, " \t"); + + pr_trace_msg(trace_channel, 22, "checking supported key type '%.*s'", + (int) len, ptr); /* field: key type */ supported_key_type = is_supported_key_type(ptr, len); @@ -303,6 +400,18 @@ int sftp_openssh_keys_parse(pool *p, const char *line, size_t linelen, *comment = pstrdup(p, ""); } + /* Now we check any options. */ + if (key_opts != NULL && + key_optslen > 0) { + unsigned int count; + + pr_trace_msg(trace_channel, 22, "checking key options '%.*s'", + (int) key_optslen, key_opts); + + count = parse_key_options(p, key_opts, key_optslen, headers); + pr_trace_msg(trace_channel, 22, "supported key options parsed: %u", count); + } + pr_trace_msg(trace_channel, 22, "successfully parsed OpenSSH key (comment '%s')", *comment); return 0; diff --git a/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_command.pub b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_command.pub new file mode 100644 index 0000000..57e87d4 --- /dev/null +++ b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_command.pub @@ -0,0 +1 @@ +command="dump /home" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDU+2NlHjoi7UHqvMTlrZuptEjLzgQF7gcYLhPMPooKKVIEsHzBnvIHGmDhMGlFo1ueJbWQGaLGn9O6nK66mPG0EmnpeIdeTIKvfpmtTceWsdBY+51iZ2CKO9FPoaGEExAnmIzJmoyUtaqrK2Kv0JH3AN8B8Nh+hwbSD7qjkLw8ur2furvZ5wh+ZuZdndu/HKc98ozYxgjd3tauZu4ZCul+MczGP2uQ8rN5oEzYx3wCk8W/awmCgWeDDjpFviX5x3uB5GB3OVWUhRPv1Djw6aw/+lgoTic9ej4qHtJwhR8pbUTGyv5TzPSv6/u4n5BDvEx/jsen2eGxsaJs7fCzdWj3 tj@golem.local diff --git a/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_from.pub b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_from.pub new file mode 100644 index 0000000..fc9a589 --- /dev/null +++ b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_from.pub @@ -0,0 +1 @@ +from="*.sales.example.net,!pc.sales.example.net" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDU+2NlHjoi7UHqvMTlrZuptEjLzgQF7gcYLhPMPooKKVIEsHzBnvIHGmDhMGlFo1ueJbWQGaLGn9O6nK66mPG0EmnpeIdeTIKvfpmtTceWsdBY+51iZ2CKO9FPoaGEExAnmIzJmoyUtaqrK2Kv0JH3AN8B8Nh+hwbSD7qjkLw8ur2furvZ5wh+ZuZdndu/HKc98ozYxgjd3tauZu4ZCul+MczGP2uQ8rN5oEzYx3wCk8W/awmCgWeDDjpFviX5x3uB5GB3OVWUhRPv1Djw6aw/+lgoTic9ej4qHtJwhR8pbUTGyv5TzPSv6/u4n5BDvEx/jsen2eGxsaJs7fCzdWj3 tj@golem.local diff --git a/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_no_touch_required.pub b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_no_touch_required.pub new file mode 100644 index 0000000..500866d --- /dev/null +++ b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_no_touch_required.pub @@ -0,0 +1 @@ +restrict,no-touch-required,permitopen="192.0.2.1:80" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDU+2NlHjoi7UHqvMTlrZuptEjLzgQF7gcYLhPMPooKKVIEsHzBnvIHGmDhMGlFo1ueJbWQGaLGn9O6nK66mPG0EmnpeIdeTIKvfpmtTceWsdBY+51iZ2CKO9FPoaGEExAnmIzJmoyUtaqrK2Kv0JH3AN8B8Nh+hwbSD7qjkLw8ur2furvZ5wh+ZuZdndu/HKc98ozYxgjd3tauZu4ZCul+MczGP2uQ8rN5oEzYx3wCk8W/awmCgWeDDjpFviX5x3uB5GB3OVWUhRPv1Djw6aw/+lgoTic9ej4qHtJwhR8pbUTGyv5TzPSv6/u4n5BDvEx/jsen2eGxsaJs7fCzdWj3 tj@golem.local diff --git a/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_verify_required.pub b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_verify_required.pub new file mode 100644 index 0000000..8abe2cd --- /dev/null +++ b/t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_verify_required.pub @@ -0,0 +1 @@ +principals="user foo",restrict,verify-required ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDU+2NlHjoi7UHqvMTlrZuptEjLzgQF7gcYLhPMPooKKVIEsHzBnvIHGmDhMGlFo1ueJbWQGaLGn9O6nK66mPG0EmnpeIdeTIKvfpmtTceWsdBY+51iZ2CKO9FPoaGEExAnmIzJmoyUtaqrK2Kv0JH3AN8B8Nh+hwbSD7qjkLw8ur2furvZ5wh+ZuZdndu/HKc98ozYxgjd3tauZu4ZCul+MczGP2uQ8rN5oEzYx3wCk8W/awmCgWeDDjpFviX5x3uB5GB3OVWUhRPv1Djw6aw/+lgoTic9ej4qHtJwhR8pbUTGyv5TzPSv6/u4n5BDvEx/jsen2eGxsaJs7fCzdWj3 tj@golem.local diff --git a/t/lib/ProFTPD/Tests/Modules/mod_sftp_openssh.pm b/t/lib/ProFTPD/Tests/Modules/mod_sftp_openssh.pm index bfe0bed..43fd053 100644 --- a/t/lib/ProFTPD/Tests/Modules/mod_sftp_openssh.pm +++ b/t/lib/ProFTPD/Tests/Modules/mod_sftp_openssh.pm @@ -56,6 +56,28 @@ my $TESTS = { test_class => [qw(forking ssh2)], }, + # OpenSSH entries with options + + openssh_auth_publickey_rsa_with_option_command => { + order => ++$order, + test_class => [qw(forking ssh2)], + }, + + openssh_auth_publickey_rsa_with_option_from => { + order => ++$order, + test_class => [qw(forking ssh2)], + }, + + openssh_auth_publickey_rsa_with_option_no_touch_required => { + order => ++$order, + test_class => [qw(forking ssh2)], + }, + + openssh_auth_publickey_rsa_with_option_verify_required => { + order => ++$order, + test_class => [qw(forking ssh2)], + }, + }; sub new { @@ -818,4 +840,476 @@ sub openssh_auth_publickey_comments_file { test_cleanup($setup->{log_file}, $ex); } +sub openssh_auth_publickey_rsa_with_option_command { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'sftp'); + + my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/ssh_host_rsa_key'); + + my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key'); + my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key.pub'); + + # Use an entry using the unsupported "command" option. + my $test_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_command.pub'); + + my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys"); + unless (copy($test_pub_key, $authorized_keys)) { + die("Can't copy $test_pub_key to $authorized_keys: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'sftp.openssh:30 ssh2:30', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPAuthorizedUserKeys openssh:~/.authorized_keys", + ], + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + require Net::SSH2; + + my $ex; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + # Allow for server startup + sleep(2); + + my $ssh2 = Net::SSH2->new(); + unless ($ssh2->connect('127.0.0.1', $port)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->auth_publickey($setup->{user}, $rsa_pub_key, + $rsa_priv_key)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str"); + } + + $ssh2->disconnect(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); +} + +sub openssh_auth_publickey_rsa_with_option_from { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'sftp'); + + my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/ssh_host_rsa_key'); + + my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key'); + my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key.pub'); + + # Use an entry using the unsupported "from" option. + my $test_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_from.pub'); + + my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys"); + unless (copy($test_pub_key, $authorized_keys)) { + die("Can't copy $test_pub_key to $authorized_keys: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'sftp.openssh:30 ssh2:30', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPAuthorizedUserKeys openssh:~/.authorized_keys", + ], + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + require Net::SSH2; + + my $ex; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + # Allow for server startup + sleep(2); + + my $ssh2 = Net::SSH2->new(); + unless ($ssh2->connect('127.0.0.1', $port)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->auth_publickey($setup->{user}, $rsa_pub_key, + $rsa_priv_key)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str"); + } + + $ssh2->disconnect(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + test_cleanup($setup->{log_file}, $ex); +} + +sub openssh_auth_publickey_rsa_with_option_no_touch_required { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'sftp'); + + my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/ssh_host_rsa_key'); + + my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key'); + my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key.pub'); + + # Use an entry using the supported "no-touch-required" option. + my $test_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_no_touch_required.pub'); + + my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys"); + unless (copy($test_pub_key, $authorized_keys)) { + die("Can't copy $test_pub_key to $authorized_keys: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'sftp.openssh:30 ssh2:30', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPAuthorizedUserKeys openssh:~/.authorized_keys", + ], + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + require Net::SSH2; + + my $ex; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + # Allow for server startup + sleep(2); + + my $ssh2 = Net::SSH2->new(); + unless ($ssh2->connect('127.0.0.1', $port)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->auth_publickey($setup->{user}, $rsa_pub_key, + $rsa_priv_key)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str"); + } + + $ssh2->disconnect(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + eval { + if (open(my $fh, "< $setup->{log_file}")) { + my $ok = 0; + + while (my $line = <$fh>) { + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($line =~ /supported key options parsed: 1/) { + $ok = 1; + last; + } + } + + close($fh); + $self->assert($ok, test_msg("Did not see expected TraceLog message")); + + } else { + die("Can't read $setup->{log_file}: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + +sub openssh_auth_publickey_rsa_with_option_verify_required { + my $self = shift; + my $tmpdir = $self->{tmpdir}; + my $setup = test_setup($tmpdir, 'sftp'); + + my $rsa_host_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/ssh_host_rsa_key'); + + my $rsa_priv_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key'); + my $rsa_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key.pub'); + + # Use an entry using the supported "verify-required" option. + my $test_pub_key = File::Spec->rel2abs('t/etc/modules/mod_sftp_openssh/test_rsa_key_with_option_verify_required.pub'); + + my $authorized_keys = File::Spec->rel2abs("$tmpdir/.authorized_keys"); + unless (copy($test_pub_key, $authorized_keys)) { + die("Can't copy $test_pub_key to $authorized_keys: $!"); + } + + my $config = { + PidFile => $setup->{pid_file}, + ScoreboardFile => $setup->{scoreboard_file}, + SystemLog => $setup->{log_file}, + TraceLog => $setup->{log_file}, + Trace => 'sftp.openssh:30 ssh2:30', + + AuthUserFile => $setup->{auth_user_file}, + AuthGroupFile => $setup->{auth_group_file}, + AuthOrder => 'mod_auth_file.c', + + IfModules => { + 'mod_delay.c' => { + DelayEngine => 'off', + }, + + 'mod_sftp.c' => [ + "SFTPEngine on", + "SFTPLog $setup->{log_file}", + "SFTPHostKey $rsa_host_key", + "SFTPAuthorizedUserKeys openssh:~/.authorized_keys", + ], + }, + }; + + my ($port, $config_user, $config_group) = config_write($setup->{config_file}, + $config); + + # Open pipes, for use between the parent and child processes. Specifically, + # the child will indicate when it's done with its test by writing a message + # to the parent. + my ($rfh, $wfh); + unless (pipe($rfh, $wfh)) { + die("Can't open pipe: $!"); + } + + require Net::SSH2; + + my $ex; + + # Fork child + $self->handle_sigchld(); + defined(my $pid = fork()) or die("Can't fork: $!"); + if ($pid) { + eval { + # Allow for server startup + sleep(2); + + my $ssh2 = Net::SSH2->new(); + unless ($ssh2->connect('127.0.0.1', $port)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("Can't connect to SSH2 server: [$err_name] ($err_code) $err_str"); + } + + unless ($ssh2->auth_publickey($setup->{user}, $rsa_pub_key, + $rsa_priv_key)) { + my ($err_code, $err_name, $err_str) = $ssh2->error(); + die("RSA publickey authentication failed: [$err_name] ($err_code) $err_str"); + } + + $ssh2->disconnect(); + }; + if ($@) { + $ex = $@; + } + + $wfh->print("done\n"); + $wfh->flush(); + + } else { + eval { server_wait($setup->{config_file}, $rfh) }; + if ($@) { + warn($@); + exit 1; + } + + exit 0; + } + + # Stop server + server_stop($setup->{pid_file}); + $self->assert_child_ok($pid); + + eval { + if (open(my $fh, "< $setup->{log_file}")) { + my $ok = 0; + + while (my $line = <$fh>) { + chomp($line); + + if ($ENV{TEST_VERBOSE}) { + print STDERR "# $line\n"; + } + + if ($line =~ /supported key options parsed: 1/) { + $ok = 1; + last; + } + } + + close($fh); + $self->assert($ok, test_msg("Did not see expected TraceLog message")); + + } else { + die("Can't read $setup->{log_file}: $!"); + } + }; + if ($@) { + $ex = $@; + } + + test_cleanup($setup->{log_file}, $ex); +} + 1;