Added support for AuthorizedKeysCommand and AuthorizedPrincipalsCommand (#409)

This commit is contained in:
Manoj Ampalam 2019-11-15 10:51:45 -08:00 committed by GitHub
parent 754f7b4885
commit 7ae6defce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 209 additions and 31 deletions

26
auth.c
View File

@ -527,7 +527,7 @@ auth_openfile(const char *file, struct passwd *pw, int strict_modes,
strerror(errno)); strerror(errno));
return NULL; return NULL;
} }
if (strict_modes && check_secure_file_permission(file, pw) != 0) { if (strict_modes && check_secure_file_permission(file, pw, 0) != 0) {
fclose(f); fclose(f);
logit("Authentication refused."); logit("Authentication refused.");
auth_debug_add("Ignored %s", file_type); auth_debug_add("Ignored %s", file_type);
@ -925,11 +925,20 @@ subprocess(const char *tag, struct passwd *pw, const char *command,
restore_uid(); restore_uid();
return 0; return 0;
} }
#ifdef WINDOWS
if (check_secure_file_permission(av[0], pw, 1) != 0) {
error("Permissions on %s:\"%s\" are too open", tag, av[0]);
restore_uid();
return 0;
}
#else
if (safe_path(av[0], &st, NULL, 0, errmsg, sizeof(errmsg)) != 0) { if (safe_path(av[0], &st, NULL, 0, errmsg, sizeof(errmsg)) != 0) {
error("Unsafe %s \"%s\": %s", tag, av[0], errmsg); error("Unsafe %s \"%s\": %s", tag, av[0], errmsg);
restore_uid(); restore_uid();
return 0; return 0;
} }
#endif
/* Prepare to keep the child's stdout if requested */ /* Prepare to keep the child's stdout if requested */
if (pipe(p) == -1) { if (pipe(p) == -1) {
error("%s: pipe: %s", tag, strerror(errno)); error("%s: pipe: %s", tag, strerror(errno));
@ -938,6 +947,20 @@ subprocess(const char *tag, struct passwd *pw, const char *command,
} }
restore_uid(); restore_uid();
#ifdef FORK_NOT_SUPPORTED
{
posix_spawn_file_actions_t actions;
pid = -1;
if (posix_spawn_file_actions_init(&actions) != 0 ||
posix_spawn_file_actions_adddup2(&actions, p[1], STDOUT_FILENO) != 0)
fatal("posix_spawn initialization failed");
else if (__posix_spawn_asuser((pid_t*)&pid, av[0], &actions, NULL, av, NULL, pw->pw_name) != 0)
fatal("posix_spawn: %s", strerror(errno));
posix_spawn_file_actions_destroy(&actions);
}
#else
switch ((pid = fork())) { switch ((pid = fork())) {
case -1: /* error */ case -1: /* error */
error("%s: fork: %s", tag, strerror(errno)); error("%s: fork: %s", tag, strerror(errno));
@ -1004,6 +1027,7 @@ subprocess(const char *tag, struct passwd *pw, const char *command,
default: /* parent */ default: /* parent */
break; break;
} }
#endif
close(p[1]); close(p[1]);
if ((flags & SSH_SUBPROCESS_STDOUT_CAPTURE) == 0) if ((flags & SSH_SUBPROCESS_STDOUT_CAPTURE) == 0)

View File

@ -311,6 +311,12 @@ check_principals_line(struct ssh *ssh, char *cp, const struct sshkey_cert *cert,
while (ep > cp && (*ep == '\n' || *ep == ' ' || *ep == '\t')) while (ep > cp && (*ep == '\n' || *ep == ' ' || *ep == '\t'))
*ep-- = '\0'; *ep-- = '\0';
#ifdef SUPPORT_CRLF
/* account for \r at line end */
if (*ep == '\r')
*ep-- = '\0';
#endif
/* /*
* If the line has internal whitespace then assume it has * If the line has internal whitespace then assume it has
* key options. * key options.

View File

@ -154,7 +154,7 @@ sshkey_perm_ok(int fd, const char *filename)
#endif #endif
#ifdef WINDOWS /*implement permission checks on Windows*/ #ifdef WINDOWS /*implement permission checks on Windows*/
if(check_secure_file_permission(filename, NULL) != 0) { if(check_secure_file_permission(filename, NULL, 0) != 0) {
error("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); error("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
error("@ WARNING: UNPROTECTED PRIVATE KEY FILE! @"); error("@ WARNING: UNPROTECTED PRIVATE KEY FILE! @");
error("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); error("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");

View File

@ -906,31 +906,64 @@ fileio_lseek(struct w32_io* pio, unsigned __int64 offset, int origin)
return 0; return 0;
} }
/* /* fdopen() to be used on pipe handles */
* fdopen implementation - use with caution static FILE*
* this implementation deviates from POSIX spec the following way fileio_fdopen_pipe(struct w32_io* pio, const char *mode)
* - the underlying file descriptor is closed automatically {
* hence no further POSIX io operations (read, write, close, etc) on the int fd_flags = 0;
* underlying file descriptor are supported FILE* ret;
*/ debug4("fdopen - io:%p", pio);
FILE*
fileio_fdopen(struct w32_io* pio, const char *mode) if (mode[1] == '\0') {
switch (*mode) {
case 'r':
fd_flags = _O_RDONLY;
break;
case 'w':
break;
case 'a':
fd_flags = _O_APPEND;
break;
default:
errno = ENOTSUP;
debug3("fdopen - ERROR unsupported mode %s", mode);
return NULL;
}
}
else {
errno = ENOTSUP;
debug3("fdopen - ERROR unsupported mode %s", mode);
return NULL;
}
int fd = _open_osfhandle((intptr_t)pio->handle, fd_flags);
if (fd == -1 || (ret = _fdopen(fd, mode)) == NULL) {
errno = EOTHER;
debug3("fdopen - ERROR:%d _open_osfhandle()", errno);
return NULL;
}
// overwrite underlying win32 handle - its expected to be closed via fclose
// and close pio
pio->handle = NULL;
int w32_close(int);
w32_close(pio->table_index);
return ret;
}
/* fdopen() to be used on file handles */
static FILE*
fileio_fdopen_disk(struct w32_io* pio, const char *mode)
{ {
wchar_t *file_path, *wmode = NULL; wchar_t *file_path, *wmode = NULL;
FILE* ret = NULL; FILE* ret = NULL;
DWORD type = 0;
debug4("fdopen - io:%p", pio); debug4("fdopen - io:%p", pio);
if ((wmode = utf8_to_utf16(mode)) == NULL) if ((wmode = utf8_to_utf16(mode)) == NULL)
goto cleanup; goto cleanup;
/* for non-disk files, just return the descriptor */
type = GetFileType(pio->handle);
if (type != FILE_TYPE_DISK) {
return _fdopen(pio->table_index, mode);
}
file_path = get_final_path_by_handle(pio->handle); file_path = get_final_path_by_handle(pio->handle);
if (!file_path) if (!file_path)
goto cleanup; goto cleanup;
@ -950,6 +983,31 @@ cleanup:
return ret; return ret;
} }
/*
* fdopen implementation - use with caution
* this implementation deviates from POSIX spec the following way
* - the underlying file descriptor is closed automatically
* hence no further POSIX io operations (read, write, close, etc) on the
* underlying file descriptor are supported
*/
FILE*
fileio_fdopen(struct w32_io* pio, const char *mode)
{
DWORD type = 0;
debug4("fdopen - io:%p", pio);
type = GetFileType(pio->handle);
if (type == FILE_TYPE_DISK) {
return fileio_fdopen_disk(pio, mode);
} else if (type == FILE_TYPE_PIPE) {
return fileio_fdopen_pipe(pio, mode);
} else {
errno = ENOTSUP;
return NULL;
}
}
void void
fileio_on_select(struct w32_io* pio, BOOL rd) fileio_on_select(struct w32_io* pio, BOOL rd)
{ {

View File

@ -45,15 +45,14 @@
* Check the owner of the file is one of these types: Local Administrators groups, system account, current user account * Check the owner of the file is one of these types: Local Administrators groups, system account, current user account
* Check the users have access permission to the file don't voilate the following rules: * Check the users have access permission to the file don't voilate the following rules:
1. no user other than local administrators group, system account, and pwd user have write permission on the file 1. no user other than local administrators group, system account, and pwd user have write permission on the file
2. sshd account can only have read permission
* Returns 0 on success and -1 on failure * Returns 0 on success and -1 on failure
*/ */
int int
check_secure_file_permission(const char *input_path, struct passwd * pw) check_secure_file_permission(const char *input_path, struct passwd * pw, int read_ok)
{ {
PSECURITY_DESCRIPTOR pSD = NULL; PSECURITY_DESCRIPTOR pSD = NULL;
wchar_t * path_utf16 = NULL; wchar_t * path_utf16 = NULL;
PSID owner_sid = NULL, user_sid = NULL; PSID owner_sid = NULL, user_sid = NULL, ti_sid = NULL;
PACL dacl = NULL; PACL dacl = NULL;
DWORD error_code = ERROR_SUCCESS; DWORD error_code = ERROR_SUCCESS;
BOOL is_valid_sid = FALSE, is_valid_acl = FALSE; BOOL is_valid_sid = FALSE, is_valid_acl = FALSE;
@ -68,6 +67,8 @@ check_secure_file_permission(const char *input_path, struct passwd * pw)
goto cleanup; goto cleanup;
} }
ti_sid = get_sid("NT SERVICE\\TrustedInstaller");
/*Get the owner sid of the file.*/ /*Get the owner sid of the file.*/
if ((error_code = GetNamedSecurityInfoW(path_utf16, SE_FILE_OBJECT, if ((error_code = GetNamedSecurityInfoW(path_utf16, SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
@ -83,8 +84,9 @@ check_secure_file_permission(const char *input_path, struct passwd * pw)
goto cleanup; goto cleanup;
} }
if (!IsWellKnownSid(owner_sid, WinBuiltinAdministratorsSid) && if (!IsWellKnownSid(owner_sid, WinBuiltinAdministratorsSid) &&
!IsWellKnownSid(owner_sid, WinLocalSystemSid) && !IsWellKnownSid(owner_sid, WinLocalSystemSid) &&
!EqualSid(owner_sid, user_sid)) { !EqualSid(owner_sid, user_sid) &&
!(ti_sid && EqualSid(owner_sid, ti_sid))) {
debug3("Bad owner on %S", path_utf16); debug3("Bad owner on %S", path_utf16);
ret = -1; ret = -1;
goto cleanup; goto cleanup;
@ -92,7 +94,6 @@ check_secure_file_permission(const char *input_path, struct passwd * pw)
/* /*
iterate all aces of the file to find out if there is voilation of the following rules: iterate all aces of the file to find out if there is voilation of the following rules:
1. no others than administrators group, system account, and current user account have write permission on the file 1. no others than administrators group, system account, and current user account have write permission on the file
2. sshd account can only have read permission
*/ */
for (DWORD i = 0; i < dacl->AceCount; i++) { for (DWORD i = 0; i < dacl->AceCount; i++) {
PVOID current_ace = NULL; PVOID current_ace = NULL;
@ -118,8 +119,12 @@ check_secure_file_permission(const char *input_path, struct passwd * pw)
/*no need to check administrators group, pwd user account, and system account*/ /*no need to check administrators group, pwd user account, and system account*/
if (IsWellKnownSid(current_trustee_sid, WinBuiltinAdministratorsSid) || if (IsWellKnownSid(current_trustee_sid, WinBuiltinAdministratorsSid) ||
IsWellKnownSid(current_trustee_sid, WinLocalSystemSid) || IsWellKnownSid(current_trustee_sid, WinLocalSystemSid) ||
EqualSid(current_trustee_sid, user_sid)) { EqualSid(current_trustee_sid, user_sid) ||
(ti_sid && EqualSid(current_trustee_sid, ti_sid))) {
continue;
} else if (read_ok && (current_access_mask & (FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA)) == 0 ) {
/* if read is allowed, allow ACES that do not give write access*/
continue; continue;
} else { } else {
@ -159,6 +164,8 @@ cleanup:
LocalFree(pSD); LocalFree(pSD);
if (user_sid) if (user_sid)
free(user_sid); free(user_sid);
if (ti_sid)
free(ti_sid);
if(path_utf16) if(path_utf16)
free(path_utf16); free(path_utf16);
return ret; return ret;

View File

@ -1810,7 +1810,7 @@ read_config_file_depth(const char *filename, struct passwd *pw,
implementation on windows to make sure the config file is owned by a user, administrators group, or LOCALSYSTEM account implementation on windows to make sure the config file is owned by a user, administrators group, or LOCALSYSTEM account
and nobody else except Administrators group, LOCALSYSTEM, and file owner account has the write permission and nobody else except Administrators group, LOCALSYSTEM, and file owner account has the write permission
*/ */
if (check_secure_file_permission(filename, pw) != 0) if (check_secure_file_permission(filename, pw, 1) != 0)
fatal("Bad owner or permissions on %s", filename); fatal("Bad owner or permissions on %s", filename);
#else #else
struct stat sb; struct stat sb;

View File

@ -0,0 +1,57 @@
If ($PSVersiontable.PSVersion.Major -le 2) {$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path}
Import-Module $PSScriptRoot\CommonUtils.psm1 -Force
$tC = 1
$tI = 0
$suite = "authorizedKeysCommand"
Describe "E2E scenarios for AuthorizedKeysCommand" -Tags "CI" {
BeforeAll {
if($OpenSSHTestInfo -eq $null)
{
Throw "`$OpenSSHTestInfo is null. Please run Set-OpenSSHTestEnvironment to set test environments."
}
$server = $OpenSSHTestInfo["Target"]
$port = 47004
$opensshbinpath = $OpenSSHTestInfo['OpenSSHBinPath']
$ssouser = $OpenSSHTestInfo["SSOUser"]
$sshdconfig = Join-Path $Global:OpenSSHTestInfo["ServiceConfigDir"] sshd_config
$testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] $suite
if(-not (Test-Path $testDir))
{
$null = New-Item $testDir -ItemType directory -Force -ErrorAction SilentlyContinue
}
}
BeforeEach {
$stderrFile=Join-Path $testDir "$tC.$tI.stderr.txt"
$stdoutFile=Join-Path $testDir "$tC.$tI.stdout.txt"
$logFile = Join-Path $testDir "$tC.$tI.log.txt"
}
AfterEach {$tI++;}
Context "$tC - basic test cases" {
BeforeAll {$tI=1}
AfterAll{$tC++}
It "$tC.$tI - keys command with %k argument" {
#override authorizedkeysfile location to an unknown location, so AuthorizedKeysCommand gets executed
$kcOutFile = Join-Path $testDir "$tC.$tI.kcout.txt"
Remove-Item -Force $kcOutFile -ErrorAction SilentlyContinue
$sshdArgs = "-d -f $sshdconfig -E $logFile -o `"AuthorizedKeysFile .fake/authorized_keys`""
$sshdArgs += " -o `"AuthorizedKeysCommand=$env:windir\system32\cmd.exe /c echo ssh-ed25519 %k & whoami > $kcOutFile`""
$sshdArgs += " -o `"AuthorizedKeysCommandUser=$ssouser`""
$sshdArgs += " -o PasswordAuthentication=no"
Start-SSHDTestDaemon -WorkDir $opensshbinpath -Arguments $sshdArgs -Port $port
$o = ssh -p $port test_target echo 1234
Stop-SSHDTestDaemon -Port $port
$o | Should Be "1234"
#check the command is run as AuthorizedKeysCommandUser
(gc $kcOutFile).Contains($ssouser) | Should Be $true
}
}
}

View File

@ -15,6 +15,9 @@ Describe "E2E scenarios for certificate authentication" -Tags "CI" {
$port = $OpenSSHTestInfo["Port"] $port = $OpenSSHTestInfo["Port"]
$pkuser = $OpenSSHTestInfo["PubKeyUser"] $pkuser = $OpenSSHTestInfo["PubKeyUser"]
$cakey = $OpenSSHTestInfo["CA_Private_Key"] $cakey = $OpenSSHTestInfo["CA_Private_Key"]
$opensshbinpath = $OpenSSHTestInfo['OpenSSHBinPath']
$ssouser = $OpenSSHTestInfo["SSOUser"]
$sshdconfig = Join-Path $Global:OpenSSHTestInfo["ServiceConfigDir"] sshd_config
$testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] $suite $testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] $suite
if(-not (Test-Path $testDir)) if(-not (Test-Path $testDir))
@ -61,6 +64,28 @@ Describe "E2E scenarios for certificate authentication" -Tags "CI" {
$o | Should Be "1234" $o | Should Be "1234"
Remove-PasswordSetting Remove-PasswordSetting
} }
It "$tC.$tI - authenticate using certificate via AuthorizedPrincipalsCommand" {
$pcOutFile = Join-Path $testDir "$tC.$tI.pcout.txt"
$logFile = Join-Path $testDir "$tC.$tI.log.txt"
Remove-Item -Force $pcOutFile -ErrorAction SilentlyContinue
$sshdArgs = "-d -f $sshdconfig -E $logFile -o `"AuthorizedKeysFile .fake/authorized_keys`""
$sshdArgs += " -o `"AuthorizedPrincipalsCommand=$env:windir\system32\cmd.exe /c echo otheruser& echo $pkuser& whoami > $pcOutFile`""
$sshdArgs += " -o `"AuthorizedPrincipalsCommandUser=$ssouser`""
$sshdArgs += " -o PasswordAuthentication=no"
Start-SSHDTestDaemon -WorkDir $opensshbinpath -Arguments $sshdArgs -Port 47004
#set up SSH_ASKPASS for key passphrase
Add-PasswordSetting -Pass $keypassphrase
$o = ssh -i $user_key -p 47004 $pkuser@$server echo 2345
Remove-PasswordSetting
Stop-SSHDTestDaemon -Port 47004
$o | Should Be "2345"
#check the command is run as AuthorizedPrincipalsCommandUser
(gc $pcOutFile).Contains($ssouser) | Should Be $true
}
} }
} }

View File

@ -127,7 +127,7 @@ Describe "Tests for ssh config" -Tags "CI" {
It "$tC.$tI-User SSHConfig-ReadConfig negative (others has permission)" { It "$tC.$tI-User SSHConfig-ReadConfig negative (others has permission)" {
#setup #setup
Repair-FilePermission -Filepath $userConfigFile -Owners $currentUserSid -FullAccessNeeded $adminsSid,$systemSid,$currentUserSid -ReadAccessNeeded $objUserSid -confirm:$false Repair-FilePermission -Filepath $userConfigFile -Owners $currentUserSid -FullAccessNeeded $adminsSid,$systemSid,$currentUserSid,$objUserSid -confirm:$false
#Run #Run
cmd /c "ssh test_target echo 1234 2> $logPath" cmd /c "ssh test_target echo 1234 2> $logPath"

View File

@ -5,5 +5,6 @@
[localhost]:47002 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMtJMxwn+iJU0X4+EC7PSj/cfcMbdP6ahhodtXx+6RHv sshtest_hostkey_ed25519 [localhost]:47002 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMtJMxwn+iJU0X4+EC7PSj/cfcMbdP6ahhodtXx+6RHv sshtest_hostkey_ed25519
[localhost]:47002 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDU+NcQ5NuRutQJoZVjDmP/vE6IYZOaE59FTUjaoZkuPl4prdOPgqAnCwSy9XtnfzPm/oe62SyYIHgj8wRzhqjMU8g8aGqfv9ryF+hpNXZrFYXIdkdxnubzfb4e70RRRoTH8P5vuY8sAn0FIRlV/3EDkSKBFy2W3InMTO6l8gbkzzkgbn1GLvH06QJVdb2PcHksSn7dJBVHWASYi3TJWWu4muI+ZNfothujxAHqjKTJuJ9apDZIc0tnkPmlifRmolSUS4OAH2KWZ+5Gwaj7gsB8bk4QuA+QCT60OCcuzCcy4FBuXvvXkM9MBe/P2KZjVLAn86SriRtoE4RI+9R9S7DV sshtest_hostkey_rsa [localhost]:47002 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDU+NcQ5NuRutQJoZVjDmP/vE6IYZOaE59FTUjaoZkuPl4prdOPgqAnCwSy9XtnfzPm/oe62SyYIHgj8wRzhqjMU8g8aGqfv9ryF+hpNXZrFYXIdkdxnubzfb4e70RRRoTH8P5vuY8sAn0FIRlV/3EDkSKBFy2W3InMTO6l8gbkzzkgbn1GLvH06QJVdb2PcHksSn7dJBVHWASYi3TJWWu4muI+ZNfothujxAHqjKTJuJ9apDZIc0tnkPmlifRmolSUS4OAH2KWZ+5Gwaj7gsB8bk4QuA+QCT60OCcuzCcy4FBuXvvXkM9MBe/P2KZjVLAn86SriRtoE4RI+9R9S7DV sshtest_hostkey_rsa
[localhost]:47003 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMtJMxwn+iJU0X4+EC7PSj/cfcMbdP6ahhodtXx+6RHv sshtest_hostkey_ed25519 [localhost]:47003 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMtJMxwn+iJU0X4+EC7PSj/cfcMbdP6ahhodtXx+6RHv sshtest_hostkey_ed25519
[localhost]:47004 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMtJMxwn+iJU0X4+EC7PSj/cfcMbdP6ahhodtXx+6RHv sshtest_hostkey_ed25519
###OpenSSHE2ETests ###OpenSSHE2ETests

View File

@ -2044,7 +2044,7 @@ process_server_config_line(ServerOptions *options, char *line,
linenum); linenum);
len = strspn(cp, WHITESPACE); len = strspn(cp, WHITESPACE);
if (*activep && options->authorized_keys_command == NULL) { if (*activep && options->authorized_keys_command == NULL) {
if (cp[len] != '/' && strcasecmp(cp + len, "none") != 0) if (!path_absolute(cp + len) && strcasecmp(cp + len, "none") != 0)
fatal("%.200s line %d: AuthorizedKeysCommand " fatal("%.200s line %d: AuthorizedKeysCommand "
"must be an absolute path", "must be an absolute path",
filename, linenum); filename, linenum);
@ -2070,7 +2070,7 @@ process_server_config_line(ServerOptions *options, char *line,
len = strspn(cp, WHITESPACE); len = strspn(cp, WHITESPACE);
if (*activep && if (*activep &&
options->authorized_principals_command == NULL) { options->authorized_principals_command == NULL) {
if (cp[len] != '/' && strcasecmp(cp + len, "none") != 0) if (!path_absolute(cp + len) && strcasecmp(cp + len, "none") != 0)
fatal("%.200s line %d: " fatal("%.200s line %d: "
"AuthorizedPrincipalsCommand must be " "AuthorizedPrincipalsCommand must be "
"an absolute path", filename, linenum); "an absolute path", filename, linenum);

View File

@ -25,5 +25,5 @@
#ifndef _SSH_FILE_PERM_H #ifndef _SSH_FILE_PERM_H
#define _SSH_FILE_PERM_H #define _SSH_FILE_PERM_H
int check_secure_file_permission(const char *, struct passwd *); int check_secure_file_permission(const char *, struct passwd *, int);
#endif /* _SSH_FILE_PERM_H */ #endif /* _SSH_FILE_PERM_H */