Skip to content
2 changes: 1 addition & 1 deletion src/build/help/help.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,7 @@
<summary>SFTP private key file.</summary>

<text>
<p>SFTP private key file used for authentication. The <setting>{[dash]}-repo-sftp-private-key-file</setting> option can be passed multiple times to specify more than one private key file.</p>
<p>SFTP private key file used for authentication. The <setting>{[dash]}-repo-sftp-private-key-file</setting> option can be passed multiple times to specify more than one private key file. If unspecified, <backrest/> will attempt authentication via any default private key files (<file>~/.ssh/id_dsa</file>, <file>~/.ssh/id_ecdsa</file>, <file>~/.ssh/id_ecdsa_sk</file>, <file>~/.ssh/id_ed25519</file>, <file>~/.ssh/id_ed25519_sk</file>, <file>~/.ssh/id_rsa</file>) that are present.</p>
<p><b>NOTE</b>: If <setting>{[dash]}-repo-sftp-public-key-file</setting> is not specified, the public key path will be generated by appending <quote>.pub</quote> to the private key path and paired with it's private key for authentication. If it is specified, then it will be paired with each private key to attempt authentication.</p>
<p><b>NOTE</b>: libssh2 versions before 1.9.0 expect a PEM format keypair, ssh-keygen -m PEM -t rsa -P <quote></quote> will generate a PEM keypair without a passphrase.</p>
</text>
Expand Down
109 changes: 65 additions & 44 deletions src/storage/sftp/storage.c
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,8 @@ storageSftpKnownHostsFilesList(const StringList *const knownHosts)
}

/***********************************************************************************************************************************
Build private key file list. privKeys requires full path and/or leading tilde path entries.
Build identity file list. If privKeys is empty build the default file list, otherwise build the list provided. privKeys
requires full path and/or leading tilde path entries.
***********************************************************************************************************************************/
static StringList *
storageSftpIdentityFilesList(const StringList *const privKeys)
Expand All @@ -620,17 +621,41 @@ storageSftpIdentityFilesList(const StringList *const privKeys)

MEM_CONTEXT_TEMP_BEGIN()
{
// Process the privKey file list entries and add them to the result list
for (unsigned int listIdx = 0; listIdx < strLstSize(privKeys); listIdx++)
if (strLstEmpty(privKeys))
{
// Get the trimmed file path and add it to the result list
const String *const filePath = strTrim(strLstGet(privKeys, listIdx));
// Create default file list, do not include non-existent files, reduces log noise
const Storage *const sshStorage = storagePosixNewP(strNewFmt("%s%s", strZ(userHome()), "/.ssh"));

StringList *const sshDefaultIdentityFiles = strLstNew();
strLstAddFmt(sshDefaultIdentityFiles, "%s%s", strZ(userHome()), "/.ssh/id_dsa");
strLstAddFmt(sshDefaultIdentityFiles, "%s%s", strZ(userHome()), "/.ssh/id_ecdsa");
strLstAddFmt(sshDefaultIdentityFiles, "%s%s", strZ(userHome()), "/.ssh/id_ecdsa_sk");
strLstAddFmt(sshDefaultIdentityFiles, "%s%s", strZ(userHome()), "/.ssh/id_ed25519");
strLstAddFmt(sshDefaultIdentityFiles, "%s%s", strZ(userHome()), "/.ssh/id_ed25519_sk");
strLstAddFmt(sshDefaultIdentityFiles, "%s%s", strZ(userHome()), "/.ssh/id_rsa");

for (unsigned int listIdx = 0; listIdx < strLstSize(sshDefaultIdentityFiles); listIdx++)
{
const String *const filePath = strLstGet(sshDefaultIdentityFiles, listIdx);

// Expand leading tilde and add to the result list
if (strBeginsWithZ(filePath, "~/"))
strLstAddFmt(result, "%s", strZ(storageSftpExpandTildePath(filePath)));
else
strLstAdd(result, filePath);
if (storageExistsP(sshStorage, filePath))
strLstAdd(result, filePath);
}
}
else
{
// Process the privKey file list entries and add them to the result list
for (unsigned int listIdx = 0; listIdx < strLstSize(privKeys); listIdx++)
{
// Get the trimmed file path and add it to the result list
const String *const filePath = strTrim(strLstGet(privKeys, listIdx));

// Expand leading tilde and add to the result list
if (strBeginsWithZ(filePath, "~/"))
strLstAddFmt(result, "%s", strZ(storageSftpExpandTildePath(filePath)));
else
strLstAdd(result, filePath);
}
}
}
MEM_CONTEXT_TEMP_END();
Expand Down Expand Up @@ -1359,56 +1384,52 @@ storageSftpNew(
libssh2_knownhost_free(knownHostsList);
}

// Attempt to authenticate with any provided private keys
// Build/normalize private keys list
StringList *const privateKeys = storageSftpIdentityFilesList(privKeys);

// If provided a public key normalize it if necessary
String *const pubKeyPath =
param.keyPub != NULL &&
regExpMatchOne(STRDEF("^ *~"), param.keyPub) ? storageSftpExpandTildePath(param.keyPub) : strDup(param.keyPub);

// Build/normalize private keys list
StringList *const privateKeys = storageSftpIdentityFilesList(privKeys);

// Attempt to authenticate with private keys
bool authSuccess = false;

if (!strLstEmpty(privateKeys))
for (unsigned int listIdx = 0; listIdx < strLstSize(privateKeys); listIdx++)
{
// Attempt to authenticate with each private key
for (unsigned int listIdx = 0; listIdx < strLstSize(privateKeys); listIdx++)
{
const String *const privateKey = strLstGet(privateKeys, listIdx);
const String *const privateKey = strLstGet(privateKeys, listIdx);

// If a public key has been provided use only that public key, otherwise use the private key with a .pub extension
do
{
rc = libssh2_userauth_publickey_fromfile(
this->session, strZ(user),
pubKeyPath != NULL ? strZ(pubKeyPath) : strZ(strCatFmt(strNew(), "%s.pub", strZ(privateKey))),
strZ(privateKey), strZNull(param.keyPassphrase));
}
while (storageSftpWaitFd(this, rc));
// If a public key has been provided use only that public key, otherwise use the private key with a .pub extension
do
{
rc = libssh2_userauth_publickey_fromfile(
this->session, strZ(user),
pubKeyPath != NULL ? strZ(pubKeyPath) : strZ(strCatFmt(strNew(),"%s.pub", strZ(privateKey))),
strZ(privateKey), strZNull(param.keyPassphrase));
}
while (storageSftpWaitFd(this, rc));

// Log the result of the authentication attempt
if (rc != 0)
{
if (rc == LIBSSH2_ERROR_EAGAIN)
LOG_DETAIL_FMT("timeout during public key authentication");
else
{
LOG_DETAIL_FMT(
"public key authentication with username %s and key %s failed [%d]", strZ(user), strZ(privateKey), rc);
}
}
// Log the result of the authentication attempt
if (rc != 0)
{
if (rc == LIBSSH2_ERROR_EAGAIN)
LOG_DETAIL_FMT("timeout during public key authentication");
else
{
authSuccess = true;

LOG_DETAIL_FMT("public key authentication with username %s and key %s succeeded", strZ(user), strZ(privateKey));
break;
LOG_DETAIL_FMT(
"public key authentication with username %s and key %s failed [%d]", strZ(user), strZ(privateKey), rc);
}
}
else
{
authSuccess = true;

LOG_DETAIL_FMT("public key authentication with username %s and key %s succeeded", strZ(user), strZ(privateKey));
break;
}
}

// Free private key list and public key path
// Free the private key list, and the public key path
strLstFree(privateKeys);
strFree(pubKeyPath);

Expand Down
10 changes: 10 additions & 0 deletions test/src/common/harnessLibSsh2.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ libssh2 authorization constants
#define KEYPUB STRDEF("/home/" TEST_USER "/.ssh/id_rsa.pub")
#define KEYPRIV_CSTR "/home/" TEST_USER "/.ssh/id_rsa"
#define KEYPUB_CSTR "/home/" TEST_USER "/.ssh/id_rsa.pub"
#define KEYPRIV_DSA_CSTR "/home/" TEST_USER "/.ssh/id_dsa"
#define KEYPUB_DSA_CSTR "/home/" TEST_USER "/.ssh/id_dsa.pub"
#define KEYPRIV_ECDSA_CSTR "/home/" TEST_USER "/.ssh/id_ecdsa"
#define KEYPUB_ECDSA_CSTR "/home/" TEST_USER "/.ssh/id_ecdsa.pub"
#define KEYPRIV_ECDSA_SK_CSTR "/home/" TEST_USER "/.ssh/id_ecdsa_sk"
#define KEYPUB_ECDSA_SK_CSTR "/home/" TEST_USER "/.ssh/id_ecdsa_sk.pub"
#define KEYPRIV_ED25519_CSTR "/home/" TEST_USER "/.ssh/id_ed25519"
#define KEYPUB_ED25519_CSTR "/home/" TEST_USER "/.ssh/id_ed25519.pub"
#define KEYPRIV_ED25519_SK_CSTR "/home/" TEST_USER "/.ssh/id_ed25519_sk"
#define KEYPUB_ED25519_SK_CSTR "/home/" TEST_USER "/.ssh/id_ed25519_sk.pub"
#define KNOWNHOSTS_FILE_CSTR "/home/" TEST_USER "/.ssh/known_hosts"
#define KNOWNHOSTS2_FILE_CSTR "/home/" TEST_USER "/.ssh/known_hosts2"
#define ETC_KNOWNHOSTS_FILE_CSTR "/etc/ssh/ssh_known_hosts"
Expand Down
92 changes: 92 additions & 0 deletions test/src/module/storage/sftpTest.c
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,55 @@ testRun(void)
hrnCfgArgRawZ(argList, cfgOptRepoSftpHostFingerprint, "3132333435363738393039383736353433323130");
HRN_CFG_LOAD(cfgCmdArchiveGet, argList);

TEST_ERROR(
storageSftpNewP(
cfgOptionIdxStr(cfgOptRepoPath, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHost, repoIdx),
cfgOptionIdxUInt(cfgOptRepoSftpHostPort, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHostUser, repoIdx),
cfgOptionUInt64(cfgOptIoTimeout), strLstNewVarLst(cfgOptionIdxLst(cfgOptRepoSftpPrivateKeyFile, repoIdx)),
cfgOptionIdxStrId(cfgOptRepoSftpHostKeyHashType, repoIdx), .modeFile = STORAGE_MODE_FILE_DEFAULT,
.modePath = STORAGE_MODE_PATH_DEFAULT, .keyPub = cfgOptionIdxStrNull(cfgOptRepoSftpPublicKeyFile, repoIdx),
.keyPassphrase = cfgOptionIdxStrNull(cfgOptRepoSftpPrivateKeyPassphrase, repoIdx),
.hostFingerprint = cfgOptionIdxStrNull(cfgOptRepoSftpHostFingerprint, repoIdx),
.hostKeyCheckType = cfgOptionIdxStrId(cfgOptRepoSftpHostKeyCheckType, repoIdx),
.knownHosts = strLstNewVarLst(cfgOptionIdxLst(cfgOptRepoSftpKnownHost, repoIdx)),
.useSshAgent = cfgOptionIdxBool(cfgOptRepoSftpUseSshAgent, repoIdx)),
ServiceError,
"failure initializing ssh-agent support [-6]: Unable to allocate space for agent connection");

// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("public key from file auth failure, no public key");

hrnLibSsh2ScriptSet((HrnLibSsh2 [])
{
{.function = HRNLIBSSH2_INIT, .param = "[0]", .resultInt = LIBSSH2_ERROR_NONE},
{.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"},
{.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE},
{.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678909876543210"},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_AGENT_INIT, .resultNull = true},
{.function = HRNLIBSSH2_SESSION_LAST_ERROR, .errMsg = (char *)"Unable to allocate space for agent connection",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = NULL}
});

// Load configuration
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test");
hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg");
hrnCfgArgRawZ(argList, cfgOptRepo, "1");
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH);
hrnCfgArgRawZ(argList, cfgOptRepoSftpHostUser, TEST_USER);
hrnCfgArgRawZ(argList, cfgOptRepoType, "sftp");
hrnCfgArgRawZ(argList, cfgOptRepoSftpHost, "localhost");
hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyHashType, "sha1");
hrnCfgArgRawZ(argList, cfgOptRepoSftpHostKeyCheckType, "fingerprint");
hrnCfgArgRawZ(argList, cfgOptRepoSftpPrivateKeyFile, "~/.ssh/id_rsa");
hrnCfgArgRawZ(argList, cfgOptRepoSftpPublicKeyFile, "~/.ssh/id_rsa.pub");
hrnCfgArgRawZ(argList, cfgOptRepoSftpHostFingerprint, "3132333435363738393039383736353433323130");
HRN_CFG_LOAD(cfgCmdArchiveGet, argList);

TEST_ERROR(
storageSftpNewP(
cfgOptionIdxStr(cfgOptRepoPath, repoIdx), cfgOptionIdxStr(cfgOptRepoSftpHost, repoIdx),
Expand Down Expand Up @@ -776,6 +825,13 @@ testRun(void)
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("libssh2_agent_userauth success - identityAgent populated full path");

Storage *storageSsh = storagePosixNewP(strNewFmt("%s%s", strZ(userHome()), "/.ssh"), .write = true);
HRN_STORAGE_PUT_EMPTY(storageSsh, KEYPRIV_DSA_CSTR);
HRN_STORAGE_PUT_EMPTY(storageSsh, KEYPRIV_ECDSA_CSTR);
HRN_STORAGE_PUT_EMPTY(storageSsh, KEYPRIV_ECDSA_SK_CSTR);
HRN_STORAGE_PUT_EMPTY(storageSsh, KEYPRIV_ED25519_CSTR);
HRN_STORAGE_PUT_EMPTY(storageSsh, KEYPRIV_CSTR);

// Load configuration
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test");
Expand All @@ -798,6 +854,21 @@ testRun(void)
{.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"},
{.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE},
{.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678909876543210"},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_DSA_CSTR "\",\"" KEYPRIV_DSA_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_ECDSA_CSTR "\",\"" KEYPRIV_ECDSA_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_ECDSA_SK_CSTR "\",\"" KEYPRIV_ECDSA_SK_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_ED25519_CSTR "\",\"" KEYPRIV_ED25519_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_AGENT_INIT},
{.function = HRNLIBSSH2_AGENT_SET_IDENTITY_PATH},
{.function = HRNLIBSSH2_AGENT_CONNECT, .resultInt = LIBSSH2_ERROR_NONE},
Expand Down Expand Up @@ -841,6 +912,21 @@ testRun(void)
{.function = HRNLIBSSH2_SESSION_INIT_EX, .param = "[null,null,null,null]"},
{.function = HRNLIBSSH2_SESSION_HANDSHAKE, .param = HANDSHAKE_PARAM, .resultInt = LIBSSH2_ERROR_NONE},
{.function = HRNLIBSSH2_HOSTKEY_HASH, .param = "[2]", .resultZ = "12345678909876543210"},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_DSA_CSTR "\",\"" KEYPRIV_DSA_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_ECDSA_CSTR "\",\"" KEYPRIV_ECDSA_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_ECDSA_SK_CSTR "\",\"" KEYPRIV_ECDSA_SK_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_ED25519_CSTR "\",\"" KEYPRIV_ED25519_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_USERAUTH_PUBLICKEY_FROMFILE_EX,
.param = "[\"" TEST_USER "\"," TEST_USER_LEN ",\"" KEYPUB_CSTR "\",\"" KEYPRIV_CSTR "\",null]",
.resultInt = LIBSSH2_ERROR_ALLOC},
{.function = HRNLIBSSH2_AGENT_INIT},
{.function = NULL}
});
Expand All @@ -861,6 +947,11 @@ testRun(void)
ServiceError,
"libssh2 version " LIBSSH2_VERSION " does not support ssh-agent identity path, requires version 1.9 or greater");
#endif
HRN_STORAGE_REMOVE(storageSsh, KEYPRIV_DSA_CSTR);
HRN_STORAGE_REMOVE(storageSsh, KEYPRIV_ECDSA_CSTR);
HRN_STORAGE_REMOVE(storageSsh, KEYPRIV_ECDSA_SK_CSTR);
HRN_STORAGE_REMOVE(storageSsh, KEYPRIV_ED25519_CSTR);
HRN_STORAGE_REMOVE(storageSsh, KEYPRIV_CSTR);

// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("known host init failure");
Expand Down Expand Up @@ -1152,6 +1243,7 @@ testRun(void)
{.function = NULL}
});

// Load configuration
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptStanza, "test");
hrnCfgArgRawZ(argList, cfgOptPgPath, "/path/to/pg");
Expand Down