mirror of
https://git.libssh.org/projects/libssh.git
synced 2026-02-10 10:26:47 +09:00
Compare commits
5 Commits
7a2a743a39
...
28c0056bca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28c0056bca | ||
|
|
7e4f08e22a | ||
|
|
aeb0b2ec6f | ||
|
|
67cf8e3702 | ||
|
|
309f36fa83 |
@@ -888,6 +888,27 @@ LIBSSH_API void *ssh_buffer_get(ssh_buffer buffer);
|
||||
LIBSSH_API uint32_t ssh_buffer_get_len(ssh_buffer buffer);
|
||||
LIBSSH_API int ssh_session_set_disconnect_message(ssh_session session, const char *message);
|
||||
|
||||
/* SSHSIG hashes data independently from the key used, so we use a new enum
|
||||
to avoid confusion. See
|
||||
https://gitlab.com/jas/ietf-sshsig-format/-/blob/cc70a225cbd695d5a6f20aaebdb4b92b0818e43a/ietf-sshsig-format.md#L137
|
||||
*/
|
||||
enum sshsig_digest_e {
|
||||
SSHSIG_DIGEST_SHA2_256 = 0,
|
||||
SSHSIG_DIGEST_SHA2_512 = 1,
|
||||
};
|
||||
|
||||
LIBSSH_API int sshsig_sign(const void *data,
|
||||
size_t data_length,
|
||||
ssh_key privkey,
|
||||
const char *sig_namespace,
|
||||
enum sshsig_digest_e hash_alg,
|
||||
char **signature);
|
||||
LIBSSH_API int sshsig_verify(const void *data,
|
||||
size_t data_length,
|
||||
const char *signature,
|
||||
const char *sig_namespace,
|
||||
ssh_key *sign_key);
|
||||
|
||||
#ifndef LIBSSH_LEGACY_0_4
|
||||
#include "libssh/legacy.h"
|
||||
#endif
|
||||
|
||||
@@ -51,6 +51,15 @@
|
||||
#define SSH_KEY_FLAG_PRIVATE 0x0002
|
||||
#define SSH_KEY_FLAG_PKCS11_URI 0x0004
|
||||
|
||||
/* Constants matching the Lightweight Secure Shell Signature Format */
|
||||
/* https://datatracker.ietf.org/doc/draft-josefsson-sshsig-format */
|
||||
#define SSHSIG_VERSION 0x01
|
||||
#define SSHSIG_MAGIC_PREAMBLE "SSHSIG"
|
||||
#define SSHSIG_MAGIC_PREAMBLE_LEN (sizeof(SSHSIG_MAGIC_PREAMBLE) - 1)
|
||||
#define SSHSIG_BEGIN_SIGNATURE "-----BEGIN SSH SIGNATURE-----"
|
||||
#define SSHSIG_END_SIGNATURE "-----END SSH SIGNATURE-----"
|
||||
#define SSHSIG_LINE_LENGTH 76
|
||||
|
||||
struct ssh_key_struct {
|
||||
enum ssh_keytypes_e type;
|
||||
int flags;
|
||||
|
||||
@@ -489,5 +489,7 @@ LIBSSH_AFTER_4_10_0
|
||||
sftp_name_id_map_free;
|
||||
sftp_name_id_map_new;
|
||||
ssh_get_supported_methods;
|
||||
sshsig_sign;
|
||||
sshsig_verify;
|
||||
} LIBSSH_4_10_0;
|
||||
|
||||
|
||||
596
src/pki.c
596
src/pki.c
@@ -405,6 +405,59 @@ bool ssh_key_size_allowed(ssh_session session, ssh_key key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Helper function to convert a key type to a hash type.
|
||||
*
|
||||
* @param[in] type The type to convert.
|
||||
*
|
||||
* @return A hash type to be used.
|
||||
*
|
||||
* @warning This helper function is available for use without session (for
|
||||
* example for signing commits) and might cause interoperability issues
|
||||
* when used within session! It is recommended to use
|
||||
* ssh_key_type_to_hash() instead of this helper directly when a
|
||||
* session is available.
|
||||
*
|
||||
* @note In order to follow current security best practises for RSA, defaults
|
||||
* to SHA-2 with SHA-512 digest (RFC8332) instead of the default for
|
||||
* the SSH protocol (SHA1 with RSA ; RFC 4253).
|
||||
*
|
||||
* @see ssh_key_type_to_hash()
|
||||
*/
|
||||
static enum ssh_digest_e key_type_to_hash(enum ssh_keytypes_e type)
|
||||
{
|
||||
switch (type) {
|
||||
case SSH_KEYTYPE_RSA_CERT01:
|
||||
case SSH_KEYTYPE_RSA:
|
||||
return SSH_DIGEST_SHA512;
|
||||
case SSH_KEYTYPE_ECDSA_P256_CERT01:
|
||||
case SSH_KEYTYPE_ECDSA_P256:
|
||||
return SSH_DIGEST_SHA256;
|
||||
case SSH_KEYTYPE_ECDSA_P384_CERT01:
|
||||
case SSH_KEYTYPE_ECDSA_P384:
|
||||
return SSH_DIGEST_SHA384;
|
||||
case SSH_KEYTYPE_ECDSA_P521_CERT01:
|
||||
case SSH_KEYTYPE_ECDSA_P521:
|
||||
return SSH_DIGEST_SHA512;
|
||||
case SSH_KEYTYPE_ED25519_CERT01:
|
||||
case SSH_KEYTYPE_ED25519:
|
||||
return SSH_DIGEST_AUTO;
|
||||
case SSH_KEYTYPE_RSA1:
|
||||
case SSH_KEYTYPE_DSS: /* deprecated */
|
||||
case SSH_KEYTYPE_DSS_CERT01: /* deprecated */
|
||||
case SSH_KEYTYPE_ECDSA:
|
||||
case SSH_KEYTYPE_UNKNOWN:
|
||||
default:
|
||||
SSH_LOG(SSH_LOG_WARN,
|
||||
"Digest algorithm to be used with key type %u "
|
||||
"is not defined",
|
||||
type);
|
||||
}
|
||||
|
||||
/* We should never reach this */
|
||||
return SSH_DIGEST_AUTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert a key type to a hash type. This is usually unambiguous
|
||||
* for all the key types, unless the SHA2 extension (RFC 8332) is
|
||||
@@ -448,26 +501,8 @@ enum ssh_digest_e ssh_key_type_to_hash(ssh_session session,
|
||||
/* Default algorithm for RSA is SHA1 */
|
||||
return SSH_DIGEST_SHA1;
|
||||
|
||||
case SSH_KEYTYPE_ECDSA_P256_CERT01:
|
||||
case SSH_KEYTYPE_ECDSA_P256:
|
||||
return SSH_DIGEST_SHA256;
|
||||
case SSH_KEYTYPE_ECDSA_P384_CERT01:
|
||||
case SSH_KEYTYPE_ECDSA_P384:
|
||||
return SSH_DIGEST_SHA384;
|
||||
case SSH_KEYTYPE_ECDSA_P521_CERT01:
|
||||
case SSH_KEYTYPE_ECDSA_P521:
|
||||
return SSH_DIGEST_SHA512;
|
||||
case SSH_KEYTYPE_ED25519_CERT01:
|
||||
case SSH_KEYTYPE_ED25519:
|
||||
return SSH_DIGEST_AUTO;
|
||||
case SSH_KEYTYPE_RSA1:
|
||||
case SSH_KEYTYPE_DSS: /* deprecated */
|
||||
case SSH_KEYTYPE_DSS_CERT01: /* deprecated */
|
||||
case SSH_KEYTYPE_ECDSA:
|
||||
case SSH_KEYTYPE_UNKNOWN:
|
||||
default:
|
||||
SSH_LOG(SSH_LOG_TRACE, "Digest algorithm to be used with key type %u "
|
||||
"is not defined", type);
|
||||
return key_type_to_hash(type);
|
||||
}
|
||||
|
||||
/* We should never reach this */
|
||||
@@ -2689,6 +2724,529 @@ ssh_signature pki_do_sign(const ssh_key privkey,
|
||||
return pki_sign_data(privkey, hash_type, input, input_len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Encodes a binary signature blob as an sshsig armored signature
|
||||
*
|
||||
* @param blob The binary signature blob to encode
|
||||
* @param out_str Pointer to store the allocated base64 encoded string
|
||||
* Must be freed with ssh_string_free_char()
|
||||
*
|
||||
* @return SSH_OK on success, SSH_ERROR on error
|
||||
*/
|
||||
static int sshsig_armor(ssh_buffer blob, char **out_str)
|
||||
{
|
||||
char *b64_data = NULL;
|
||||
char *armored = NULL;
|
||||
const unsigned char *data = NULL;
|
||||
size_t len, b64_len, armored_len, num_lines;
|
||||
size_t i, j;
|
||||
|
||||
if (blob == NULL || out_str == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Invalid input parameters");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
*out_str = NULL;
|
||||
|
||||
data = ssh_buffer_get(blob);
|
||||
len = ssh_buffer_get_len(blob);
|
||||
|
||||
b64_data = (char *)bin_to_base64(data, len);
|
||||
if (b64_data == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to base64 encode signature blob");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
b64_len = strlen(b64_data);
|
||||
|
||||
/* Calculate space needed: header + data with line breaks + footer */
|
||||
num_lines = (b64_len + SSHSIG_LINE_LENGTH - 1) /
|
||||
SSHSIG_LINE_LENGTH; /* Round up division */
|
||||
armored_len = strlen(SSHSIG_BEGIN_SIGNATURE) + 1 + /* header + \n */
|
||||
b64_len + num_lines + /* data + line breaks */
|
||||
strlen(SSHSIG_END_SIGNATURE) + 1; /* footer + \0 */
|
||||
|
||||
armored = calloc(armored_len, 1);
|
||||
if (armored == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Failed to allocate %zu bytes for armored signature",
|
||||
armored_len);
|
||||
SAFE_FREE(b64_data);
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
j = snprintf(armored, armored_len, SSHSIG_BEGIN_SIGNATURE "\n");
|
||||
for (i = 0; i < b64_len; i++) {
|
||||
if (i > 0 && i % SSHSIG_LINE_LENGTH == 0) {
|
||||
armored[j++] = '\n';
|
||||
}
|
||||
armored[j++] = b64_data[i];
|
||||
}
|
||||
armored[j++] = '\n';
|
||||
snprintf(armored + j, armored_len - j, SSHSIG_END_SIGNATURE);
|
||||
|
||||
SAFE_FREE(b64_data);
|
||||
|
||||
*out_str = armored;
|
||||
return SSH_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Dearmor an sshsig signature from ASCII armored format to binary
|
||||
*
|
||||
* @param[in] signature The armored sshsig signature string
|
||||
* @param[out] out Pointer to store the allocated binary buffer
|
||||
*
|
||||
* @return SSH_OK on success, SSH_ERROR on error
|
||||
*/
|
||||
static int sshsig_dearmor(const char *signature, ssh_buffer *out)
|
||||
{
|
||||
const char *begin = NULL;
|
||||
const char *end = NULL;
|
||||
char *clean_b64 = NULL;
|
||||
ssh_buffer decoded_buffer = NULL;
|
||||
int i, j;
|
||||
int rc = SSH_ERROR;
|
||||
|
||||
if (signature == NULL || out == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Invalid input parameters");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
*out = NULL;
|
||||
|
||||
rc = strncmp(signature,
|
||||
SSHSIG_BEGIN_SIGNATURE,
|
||||
strlen(SSHSIG_BEGIN_SIGNATURE));
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Signature does not start with expected header");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
begin = signature + strlen(SSHSIG_BEGIN_SIGNATURE);
|
||||
while (isspace(*begin)) {
|
||||
begin++;
|
||||
}
|
||||
|
||||
end = strstr(begin, SSHSIG_END_SIGNATURE);
|
||||
if (end == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Signature end marker not found");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
/* Backtrack to find the real end of data */
|
||||
while (end > begin && (isspace(*(end - 1)))) {
|
||||
end--;
|
||||
}
|
||||
|
||||
clean_b64 = calloc(end - begin + 1, 1);
|
||||
if (clean_b64 == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Failed to allocate %td bytes for clean base64 data",
|
||||
end - begin + 1);
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
for (i = 0, j = 0; begin + i < end; i++) {
|
||||
if (!isspace(begin[i])) {
|
||||
clean_b64[j++] = begin[i];
|
||||
}
|
||||
}
|
||||
clean_b64[j] = '\0';
|
||||
|
||||
decoded_buffer = base64_to_bin(clean_b64);
|
||||
SAFE_FREE(clean_b64);
|
||||
|
||||
if (decoded_buffer == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to decode base64 signature data");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
*out = decoded_buffer;
|
||||
return SSH_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @brief Common helper function to prepare the data in sshsig format
|
||||
*
|
||||
* This function handles the common logic to prepare the sshsig format:
|
||||
* 1. Hash the input data using the specified algorithm
|
||||
* 2. Build the data buffer to sign
|
||||
*
|
||||
* @param data The raw data to process
|
||||
* @param data_length The length of the data
|
||||
* @param hash_alg The hash algorithm to use (sha256 or sha512)
|
||||
* @param sig_namespace The signature namespace
|
||||
* @param tosign_buf Pointer to store the allocated to-sign buffer
|
||||
*
|
||||
* @return SSH_OK on success, SSH_ERROR on error
|
||||
*/
|
||||
static int sshsig_prepare_data(const void *data,
|
||||
size_t data_length,
|
||||
const char *hash_alg,
|
||||
const char *sig_namespace,
|
||||
ssh_buffer *tosign_buf)
|
||||
{
|
||||
ssh_buffer tosign = NULL;
|
||||
ssh_string hash_string = NULL;
|
||||
char hash[SHA512_DIGEST_LEN];
|
||||
size_t hash_len;
|
||||
int rc = SSH_ERROR;
|
||||
|
||||
if (data == NULL || hash_alg == NULL || sig_namespace == NULL ||
|
||||
tosign_buf == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Invalid input parameters");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
*tosign_buf = NULL;
|
||||
|
||||
if (strcmp(hash_alg, "sha256") == 0) {
|
||||
hash_len = SHA256_DIGEST_LEN;
|
||||
rc = sha256(data, data_length, (unsigned char *)hash);
|
||||
} else if (strcmp(hash_alg, "sha512") == 0) {
|
||||
hash_len = SHA512_DIGEST_LEN;
|
||||
rc = sha512(data, data_length, (unsigned char *)hash);
|
||||
} else {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Unsupported hash algorithm: %s", hash_alg);
|
||||
goto cleanup;
|
||||
}
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to compute %s hash of data", hash_alg);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
hash_string = ssh_string_new(hash_len);
|
||||
if (hash_string == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to allocate ssh_string for hash");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = ssh_string_fill(hash_string, hash, hash_len);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to fill ssh_string with hash data");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
tosign = ssh_buffer_new();
|
||||
if (tosign == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to allocate buffer for signing data");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = ssh_buffer_pack(tosign,
|
||||
"tsssS",
|
||||
SSHSIG_MAGIC_PREAMBLE,
|
||||
sig_namespace,
|
||||
"",
|
||||
hash_alg,
|
||||
hash_string);
|
||||
|
||||
if (rc == SSH_OK) {
|
||||
*tosign_buf = tosign;
|
||||
tosign = NULL;
|
||||
} else {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to pack signing data into buffer");
|
||||
}
|
||||
|
||||
cleanup:
|
||||
SSH_BUFFER_FREE(tosign);
|
||||
SSH_STRING_FREE(hash_string);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Signs data in sshsig compatible format
|
||||
*
|
||||
* @param data The data to sign
|
||||
* @param data_length The length of the data
|
||||
* @param privkey The private key to sign with
|
||||
* @param hash_alg The hash algorithm to use (SSHSIG_DIGEST_SHA2_256 or
|
||||
* SSHSIG_DIGEST_SHA2_512)
|
||||
* @param sig_namespace The signature namespace (e.g. "file", "email", etc.)
|
||||
* @param signature Pointer to store the allocated signature string in the
|
||||
* armored format. Must be freed with
|
||||
* ssh_string_free_char()
|
||||
*
|
||||
* @return SSH_OK on success, SSH_ERROR on error
|
||||
*/
|
||||
int sshsig_sign(const void *data,
|
||||
size_t data_length,
|
||||
ssh_key privkey,
|
||||
const char *sig_namespace,
|
||||
enum sshsig_digest_e hash_alg,
|
||||
char **signature)
|
||||
{
|
||||
ssh_buffer tosign = NULL;
|
||||
ssh_buffer signature_blob = NULL;
|
||||
ssh_signature sig = NULL;
|
||||
ssh_string sig_string = NULL;
|
||||
ssh_string pub_blob = NULL;
|
||||
enum ssh_digest_e digest_type;
|
||||
const char *hash_alg_str = NULL;
|
||||
int rc = SSH_ERROR;
|
||||
|
||||
if (privkey == NULL || data == NULL || sig_namespace == NULL ||
|
||||
signature == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Invalid parameters provided to sshsig_sign");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (strlen(sig_namespace) == 0) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Invalid parameters provided to sshsig_sign: empty namespace "
|
||||
"string");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
*signature = NULL;
|
||||
|
||||
if (hash_alg == SSHSIG_DIGEST_SHA2_256) {
|
||||
hash_alg_str = "sha256";
|
||||
} else if (hash_alg == SSHSIG_DIGEST_SHA2_512) {
|
||||
hash_alg_str = "sha512";
|
||||
} else {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Invalid hash algorithm %d", hash_alg);
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
rc = sshsig_prepare_data(data,
|
||||
data_length,
|
||||
hash_alg_str,
|
||||
sig_namespace,
|
||||
&tosign);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to prepare data for sshsig signing");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
digest_type = key_type_to_hash(ssh_key_type_plain(privkey->type));
|
||||
sig = pki_sign_data(privkey,
|
||||
digest_type,
|
||||
ssh_buffer_get(tosign),
|
||||
ssh_buffer_get_len(tosign));
|
||||
if (sig == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to sign data with private key");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = ssh_pki_export_pubkey_blob(privkey, &pub_blob);
|
||||
if (rc != SSH_OK || pub_blob == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Failed to export public key blob from private key");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = ssh_pki_export_signature_blob(sig, &sig_string);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to export signature blob");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
signature_blob = ssh_buffer_new();
|
||||
if (signature_blob == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to allocate signature buffer");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = ssh_buffer_pack(signature_blob,
|
||||
"tdSsssS",
|
||||
SSHSIG_MAGIC_PREAMBLE,
|
||||
SSHSIG_VERSION,
|
||||
pub_blob,
|
||||
sig_namespace,
|
||||
"",
|
||||
hash_alg_str,
|
||||
sig_string);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to pack signature blob");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = sshsig_armor(signature_blob, signature);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to armor signature blob");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
SSH_BUFFER_FREE(tosign);
|
||||
SSH_BUFFER_FREE(signature_blob);
|
||||
SSH_SIGNATURE_FREE(sig);
|
||||
SSH_STRING_FREE(sig_string);
|
||||
SSH_STRING_FREE(pub_blob);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Verifies an sshsig formatted signature against data
|
||||
*
|
||||
* @param data The data to verify
|
||||
* @param data_length The length of the data
|
||||
* @param signature The armored sshsig signature
|
||||
* @param sig_namespace The expected signature namespace
|
||||
* @param sign_key If not NULL, returns the allocated public key that was
|
||||
* used for signing this data. Must be freed with
|
||||
* ssh_key_free(). Note that this is an output parameter
|
||||
* and is not checked against "allowed signers". The
|
||||
* caller needs to compare it with expected signer key
|
||||
* using ssh_key_cmp().
|
||||
*
|
||||
* @return SSH_OK on success, SSH_ERROR on verification failure
|
||||
*/
|
||||
int sshsig_verify(const void *data,
|
||||
size_t data_length,
|
||||
const char *signature,
|
||||
const char *sig_namespace,
|
||||
ssh_key *sign_key)
|
||||
{
|
||||
ssh_buffer sig_buf = NULL;
|
||||
ssh_buffer tosign = NULL;
|
||||
ssh_key key = NULL;
|
||||
char *hash_alg_str = NULL;
|
||||
ssh_string sig_data = NULL;
|
||||
ssh_string sig_namespace_str = NULL;
|
||||
ssh_string reserved_str = NULL;
|
||||
ssh_string pubkey_blob = NULL;
|
||||
int rc = SSH_ERROR;
|
||||
ssh_signature signature_obj = NULL;
|
||||
uint32_t sig_version;
|
||||
|
||||
if (sign_key != NULL) {
|
||||
*sign_key = NULL;
|
||||
}
|
||||
|
||||
if (signature == NULL || data == NULL || sig_namespace == NULL) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Invalid parameters provided to sshsig_verify");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (strlen(sig_namespace) == 0) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Invalid parameters provided to sshsig_verify: empty namespace "
|
||||
"string");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
rc = sshsig_dearmor(signature, &sig_buf);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to dearmor signature");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (ssh_buffer_get_len(sig_buf) < SSHSIG_MAGIC_PREAMBLE_LEN ||
|
||||
memcmp(ssh_buffer_get(sig_buf),
|
||||
SSHSIG_MAGIC_PREAMBLE,
|
||||
SSHSIG_MAGIC_PREAMBLE_LEN) != 0) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Invalid signature magic preamble");
|
||||
SSH_BUFFER_FREE(sig_buf);
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
ssh_buffer_pass_bytes(sig_buf, SSHSIG_MAGIC_PREAMBLE_LEN);
|
||||
rc = ssh_buffer_unpack(sig_buf,
|
||||
"dSSSsS",
|
||||
&sig_version,
|
||||
&pubkey_blob,
|
||||
&sig_namespace_str,
|
||||
&reserved_str,
|
||||
&hash_alg_str,
|
||||
&sig_data);
|
||||
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to unpack signature buffer");
|
||||
SSH_BUFFER_FREE(sig_buf);
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (sig_version != SSHSIG_VERSION) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Unsupported signature version %u, expected %u",
|
||||
sig_version,
|
||||
SSHSIG_VERSION);
|
||||
rc = SSH_ERROR;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = ssh_pki_import_pubkey_blob(pubkey_blob, &key);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to import public key from signature");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (ssh_string_len(sig_namespace_str) != strlen(sig_namespace) ||
|
||||
memcmp(ssh_string_data(sig_namespace_str),
|
||||
sig_namespace,
|
||||
strlen(sig_namespace)) != 0) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Signature namespace mismatch: expected '%s', got '%s'",
|
||||
sig_namespace,
|
||||
ssh_string_get_char(sig_namespace_str));
|
||||
rc = SSH_ERROR;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (strcmp(hash_alg_str, "sha256") != 0 &&
|
||||
strcmp(hash_alg_str, "sha512") != 0) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Unsupported hash algorithm '%s'", hash_alg_str);
|
||||
rc = SSH_ERROR;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = sshsig_prepare_data(data,
|
||||
data_length,
|
||||
hash_alg_str,
|
||||
sig_namespace,
|
||||
&tosign);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Failed to prepare data for sshsig verification");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = ssh_pki_import_signature_blob(sig_data, key, &signature_obj);
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Failed to import signature blob");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
rc = pki_verify_data_signature(signature_obj,
|
||||
key,
|
||||
ssh_buffer_get(tosign),
|
||||
ssh_buffer_get_len(tosign));
|
||||
if (rc != SSH_OK) {
|
||||
SSH_LOG(SSH_LOG_TRACE, "Signature verification failed");
|
||||
goto cleanup;
|
||||
}
|
||||
if (strlen(sig_namespace) == 0) {
|
||||
SSH_LOG(SSH_LOG_TRACE,
|
||||
"Invalid parameters provided to sshsig_verify: empty namespace "
|
||||
"string");
|
||||
return SSH_ERROR;
|
||||
}
|
||||
|
||||
if (sign_key != NULL) {
|
||||
*sign_key = key;
|
||||
key = NULL; /* Transferred ownership */
|
||||
}
|
||||
|
||||
cleanup:
|
||||
SSH_STRING_FREE(pubkey_blob);
|
||||
SSH_STRING_FREE(sig_namespace_str);
|
||||
SSH_STRING_FREE(reserved_str);
|
||||
SSH_STRING_FREE(sig_data);
|
||||
SSH_BUFFER_FREE(tosign);
|
||||
SSH_BUFFER_FREE(sig_buf);
|
||||
SSH_KEY_FREE(key);
|
||||
SAFE_FREE(hash_alg_str);
|
||||
SSH_SIGNATURE_FREE(signature_obj);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
* This function signs the session id as a string then
|
||||
* the content of sigbuf */
|
||||
|
||||
@@ -102,12 +102,20 @@ add_subdirectory(unittests)
|
||||
|
||||
# OpenSSH Capabilities are required for all unit tests
|
||||
find_program(SSH_EXECUTABLE NAMES ssh)
|
||||
find_program(SSH_KEYGEN_EXECUTABLE NAMES ssh-keygen)
|
||||
if (SSH_EXECUTABLE)
|
||||
execute_process(COMMAND ${SSH_EXECUTABLE} -V ERROR_VARIABLE OPENSSH_VERSION_STR)
|
||||
string(REGEX REPLACE "^.*OpenSSH_([0-9]+).[0-9].*$" "\\1" OPENSSH_VERSION_MAJOR "${OPENSSH_VERSION_STR}")
|
||||
string(REGEX REPLACE "^.*OpenSSH_[0-9]+.([0-9]).*$" "\\1" OPENSSH_VERSION_MINOR "${OPENSSH_VERSION_STR}")
|
||||
set(OPENSSH_VERSION "${OPENSSH_VERSION_MAJOR}.${OPENSSH_VERSION_MINOR}")
|
||||
add_definitions(-DOPENSSH_VERSION_MAJOR=${OPENSSH_VERSION_MAJOR} -DOPENSSH_VERSION_MINOR=${OPENSSH_VERSION_MINOR})
|
||||
if("${OPENSSH_VERSION}" VERSION_GREATER_EQUAL "8.1" AND SSH_KEYGEN_EXECUTABLE)
|
||||
set(OPENSSH_SUPPORTS_SSHSIG 1)
|
||||
message(STATUS "OpenSSH ${OPENSSH_VERSION} supports SSH signatures")
|
||||
else()
|
||||
set(OPENSSH_SUPPORTS_SSHSIG 0)
|
||||
message(STATUS "OpenSSH ${OPENSSH_VERSION} does not support SSH signatures (requires 8.1+)")
|
||||
endif()
|
||||
if("${OPENSSH_VERSION}" VERSION_LESS "6.3")
|
||||
# ssh - Q was introduced in 6.3
|
||||
message("Version less than 6.3, hardcoding cipher list")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#cmakedefine OPENSSH_VERSION_MAJOR ${OPENSSH_VERSION_MAJOR}
|
||||
#cmakedefine OPENSSH_VERSION_MINOR ${OPENSSH_VERSION_MINOR}
|
||||
#cmakedefine OPENSSH_SUPPORTS_SSHSIG ${OPENSSH_SUPPORTS_SSHSIG}
|
||||
|
||||
#cmakedefine OPENSSH_CIPHERS "${OPENSSH_CIPHERS}"
|
||||
#cmakedefine OPENSSH_MACS "${OPENSSH_MACS}"
|
||||
@@ -66,6 +67,7 @@
|
||||
#cmakedefine NCAT_EXECUTABLE "${NCAT_EXECUTABLE}"
|
||||
#cmakedefine SSHD_EXECUTABLE "${SSHD_EXECUTABLE}"
|
||||
#cmakedefine SSH_EXECUTABLE "${SSH_EXECUTABLE}"
|
||||
#cmakedefine SSH_KEYGEN_EXECUTABLE "${SSH_KEYGEN_EXECUTABLE}"
|
||||
#cmakedefine WITH_TIMEOUT ${WITH_TIMEOUT}
|
||||
#cmakedefine TIMEOUT_EXECUTABLE "${TIMEOUT_EXECUTABLE}"
|
||||
#cmakedefine SOFTHSM2_LIBRARY "${SOFTHSM2_LIBRARY}"
|
||||
|
||||
@@ -47,6 +47,7 @@ if (UNIX AND NOT WIN32)
|
||||
torture_pki_rsa
|
||||
torture_pki_dsa
|
||||
torture_pki_ed25519
|
||||
torture_pki_sshsig
|
||||
# requires /dev/null
|
||||
torture_channel
|
||||
)
|
||||
|
||||
605
tests/unittests/torture_pki_sshsig.c
Normal file
605
tests/unittests/torture_pki_sshsig.c
Normal file
@@ -0,0 +1,605 @@
|
||||
#include "config.h"
|
||||
|
||||
#define LIBSSH_STATIC
|
||||
|
||||
#include "libssh/pki.h"
|
||||
#include "pki.c"
|
||||
#include "torture.h"
|
||||
#include "torture_key.h"
|
||||
#include "torture_pki.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static const char template[] = "tmp_XXXXXX";
|
||||
static const char input[] = "Test input\0string with null byte";
|
||||
static const size_t input_len = sizeof(input) - 1; /* -1 to exclude final \0 */
|
||||
static const char *test_namespace = "file";
|
||||
|
||||
struct key_hash_combo {
|
||||
enum ssh_keytypes_e key_type;
|
||||
enum sshsig_digest_e hash_alg;
|
||||
const char *key_name;
|
||||
};
|
||||
|
||||
struct sshsig_st {
|
||||
/*
|
||||
* The original current working directory at the start of the test.
|
||||
*
|
||||
* During setup, the current working directory is changed to a newly
|
||||
* created temporary directory (temp_dir).
|
||||
*
|
||||
* During cleanup, the current working directory is restored back
|
||||
* to original_cwd.
|
||||
*/
|
||||
char *original_cwd;
|
||||
char *temp_dir;
|
||||
ssh_key rsa_key;
|
||||
ssh_key ed25519_key;
|
||||
ssh_key ecdsa_key;
|
||||
const char *ssh_keygen_path;
|
||||
const struct key_hash_combo *test_combinations;
|
||||
size_t num_combinations;
|
||||
};
|
||||
|
||||
static struct key_hash_combo test_combinations[] = {
|
||||
{SSH_KEYTYPE_RSA, SSHSIG_DIGEST_SHA2_256, "rsa"},
|
||||
{SSH_KEYTYPE_RSA, SSHSIG_DIGEST_SHA2_512, "rsa"},
|
||||
{SSH_KEYTYPE_ED25519, SSHSIG_DIGEST_SHA2_256, "ed25519"},
|
||||
{SSH_KEYTYPE_ED25519, SSHSIG_DIGEST_SHA2_512, "ed25519"},
|
||||
#ifdef HAVE_ECC
|
||||
{SSH_KEYTYPE_ECDSA_P256, SSHSIG_DIGEST_SHA2_256, "ecdsa"},
|
||||
{SSH_KEYTYPE_ECDSA_P256, SSHSIG_DIGEST_SHA2_512, "ecdsa"},
|
||||
#endif
|
||||
};
|
||||
|
||||
static ssh_key get_test_key(struct sshsig_st *test_state,
|
||||
enum ssh_keytypes_e type)
|
||||
{
|
||||
switch (type) {
|
||||
case SSH_KEYTYPE_RSA:
|
||||
return test_state->rsa_key;
|
||||
case SSH_KEYTYPE_ED25519:
|
||||
if (ssh_fips_mode()) {
|
||||
return NULL;
|
||||
} else {
|
||||
return test_state->ed25519_key;
|
||||
}
|
||||
#ifdef HAVE_ECC
|
||||
case SSH_KEYTYPE_ECDSA_P256:
|
||||
return test_state->ecdsa_key;
|
||||
#endif
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static int setup_sshsig_compat(void **state)
|
||||
{
|
||||
struct sshsig_st *test_state = NULL;
|
||||
char *original_cwd = NULL;
|
||||
char *temp_dir = NULL;
|
||||
int rc = 0;
|
||||
|
||||
test_state = calloc(1, sizeof(struct sshsig_st));
|
||||
assert_non_null(test_state);
|
||||
|
||||
original_cwd = torture_get_current_working_dir();
|
||||
assert_non_null(original_cwd);
|
||||
|
||||
temp_dir = torture_make_temp_dir(template);
|
||||
assert_non_null(temp_dir);
|
||||
|
||||
test_state->original_cwd = original_cwd;
|
||||
test_state->temp_dir = temp_dir;
|
||||
test_state->test_combinations = test_combinations;
|
||||
test_state->num_combinations =
|
||||
sizeof(test_combinations) / sizeof(test_combinations[0]);
|
||||
|
||||
*state = test_state;
|
||||
|
||||
rc = torture_change_dir(temp_dir);
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
/* Check if openssh is available and supports SSH signatures */
|
||||
#ifdef OPENSSH_SUPPORTS_SSHSIG
|
||||
test_state->ssh_keygen_path = SSH_KEYGEN_EXECUTABLE;
|
||||
#else
|
||||
test_state->ssh_keygen_path = NULL;
|
||||
printf("OpenSSH version does not support SSH signatures (requires "
|
||||
"8.1+), skipping compatibility tests\n");
|
||||
#endif /* OPENSSH_SUPPORTS_SSHSIG */
|
||||
|
||||
/* Load pre-generated test keys using torture functions */
|
||||
rc = ssh_pki_import_privkey_base64(torture_get_testkey(SSH_KEYTYPE_RSA, 0),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&test_state->rsa_key);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
|
||||
/* Skip ed25519 if in FIPS mode */
|
||||
if (!ssh_fips_mode()) {
|
||||
/* mbedtls and libgcrypt don't fully support PKCS#8 PEM */
|
||||
/* thus parse the key with OpenSSH */
|
||||
rc = ssh_pki_import_privkey_base64(
|
||||
torture_get_openssh_testkey(SSH_KEYTYPE_ED25519, 0),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&test_state->ed25519_key);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
}
|
||||
|
||||
#ifdef HAVE_ECC
|
||||
rc = ssh_pki_import_privkey_base64(
|
||||
torture_get_testkey(SSH_KEYTYPE_ECDSA_P256, 0),
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&test_state->ecdsa_key);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
#endif
|
||||
|
||||
/* Write keys to files for openssh compatibility testing */
|
||||
if (test_state->ssh_keygen_path != NULL) {
|
||||
torture_write_file("test_rsa", torture_get_testkey(SSH_KEYTYPE_RSA, 0));
|
||||
torture_write_file("test_rsa.pub",
|
||||
torture_get_testkey_pub(SSH_KEYTYPE_RSA));
|
||||
|
||||
if (!ssh_fips_mode()) {
|
||||
torture_write_file(
|
||||
"test_ed25519",
|
||||
torture_get_openssh_testkey(SSH_KEYTYPE_ED25519, 0));
|
||||
torture_write_file("test_ed25519.pub",
|
||||
torture_get_testkey_pub(SSH_KEYTYPE_ED25519));
|
||||
}
|
||||
|
||||
#ifdef HAVE_ECC
|
||||
torture_write_file("test_ecdsa",
|
||||
torture_get_testkey(SSH_KEYTYPE_ECDSA_P256, 0));
|
||||
torture_write_file("test_ecdsa.pub",
|
||||
torture_get_testkey_pub(SSH_KEYTYPE_ECDSA_P256));
|
||||
#endif
|
||||
|
||||
rc = chmod("test_rsa", 0600);
|
||||
assert_return_code(rc, errno);
|
||||
if (!ssh_fips_mode()) {
|
||||
rc = chmod("test_ed25519", 0600);
|
||||
assert_return_code(rc, errno);
|
||||
}
|
||||
#ifdef HAVE_ECC
|
||||
rc = chmod("test_ecdsa", 0600);
|
||||
assert_return_code(rc, errno);
|
||||
#endif
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int teardown_sshsig_compat(void **state)
|
||||
{
|
||||
struct sshsig_st *test_state = *state;
|
||||
int rc = 0;
|
||||
|
||||
assert_non_null(test_state);
|
||||
|
||||
ssh_key_free(test_state->rsa_key);
|
||||
ssh_key_free(test_state->ed25519_key);
|
||||
ssh_key_free(test_state->ecdsa_key);
|
||||
|
||||
rc = torture_change_dir(test_state->original_cwd);
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = torture_rmdirs(test_state->temp_dir);
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
SAFE_FREE(test_state->temp_dir);
|
||||
SAFE_FREE(test_state->original_cwd);
|
||||
SAFE_FREE(test_state);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int run_openssh_command(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
return WIFEXITED(rc) ? WEXITSTATUS(rc) : -1;
|
||||
}
|
||||
|
||||
static void torture_pki_sshsig_armor_dearmor(UNUSED_PARAM(void **state))
|
||||
{
|
||||
ssh_buffer test_buffer = NULL;
|
||||
ssh_buffer dearmored_buffer = NULL;
|
||||
char *armored_sig = NULL;
|
||||
const char test_data[] = "test signature data";
|
||||
int rc;
|
||||
|
||||
test_buffer = ssh_buffer_new();
|
||||
assert_non_null(test_buffer);
|
||||
|
||||
rc = ssh_buffer_add_data(test_buffer, test_data, strlen(test_data));
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
|
||||
rc = sshsig_armor(test_buffer, &armored_sig);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
assert_non_null(armored_sig);
|
||||
|
||||
/* Test with NULL armored_sig */
|
||||
rc = sshsig_armor(test_buffer, NULL);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
|
||||
assert_non_null(strstr(armored_sig, SSHSIG_BEGIN_SIGNATURE));
|
||||
assert_non_null(strstr(armored_sig, SSHSIG_END_SIGNATURE));
|
||||
|
||||
/* Test with NULL dearmored_buffer */
|
||||
rc = sshsig_dearmor(armored_sig, NULL);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
|
||||
rc = sshsig_dearmor(armored_sig, &dearmored_buffer);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
assert_non_null(dearmored_buffer);
|
||||
|
||||
assert_int_equal(ssh_buffer_get_len(test_buffer),
|
||||
ssh_buffer_get_len(dearmored_buffer));
|
||||
assert_memory_equal(ssh_buffer_get(test_buffer),
|
||||
ssh_buffer_get(dearmored_buffer),
|
||||
ssh_buffer_get_len(test_buffer));
|
||||
|
||||
ssh_buffer_free(test_buffer);
|
||||
ssh_buffer_free(dearmored_buffer);
|
||||
free(armored_sig);
|
||||
}
|
||||
|
||||
static void torture_pki_sshsig_armor_dearmor_invalid(UNUSED_PARAM(void **state))
|
||||
{
|
||||
ssh_buffer dearmored_buffer = NULL;
|
||||
char *armored_sig = NULL;
|
||||
int rc;
|
||||
const char *invalid_sig = "-----BEGIN INVALID SIGNATURE-----\n"
|
||||
"data\n"
|
||||
"-----END INVALID SIGNATURE-----\n";
|
||||
|
||||
const char *incomplete_sig = "-----BEGIN SSH SIGNATURE----\n"
|
||||
"U1NIU0lH\n";
|
||||
|
||||
/* Test with NULL buffer */
|
||||
rc = sshsig_armor(NULL, &armored_sig);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
|
||||
/* Test dearmoring with invalid signature */
|
||||
rc = sshsig_dearmor(invalid_sig, &dearmored_buffer);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
|
||||
/* Test dearmoring with NULL input */
|
||||
rc = sshsig_dearmor(NULL, &dearmored_buffer);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
|
||||
/* Test dearmoring with missing end marker */
|
||||
rc = sshsig_dearmor(incomplete_sig, &dearmored_buffer);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
}
|
||||
|
||||
static void test_libssh_sign_verify_combo(struct sshsig_st *test_state,
|
||||
const struct key_hash_combo *combo)
|
||||
{
|
||||
char *signature = NULL;
|
||||
ssh_key verify_key = NULL;
|
||||
ssh_key test_key = NULL;
|
||||
int rc;
|
||||
|
||||
if (combo->key_type == SSH_KEYTYPE_ED25519 && ssh_fips_mode()) {
|
||||
skip();
|
||||
}
|
||||
|
||||
test_key = get_test_key(test_state, combo->key_type);
|
||||
assert_non_null(test_key);
|
||||
|
||||
rc = sshsig_sign(input,
|
||||
input_len,
|
||||
test_key,
|
||||
test_namespace,
|
||||
combo->hash_alg,
|
||||
&signature);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
assert_non_null(signature);
|
||||
|
||||
rc =
|
||||
sshsig_verify(input, input_len, signature, test_namespace, &verify_key);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
assert_non_null(verify_key);
|
||||
|
||||
rc = ssh_key_cmp(test_key, verify_key, SSH_KEY_CMP_PUBLIC);
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
ssh_key_free(verify_key);
|
||||
free(signature);
|
||||
}
|
||||
|
||||
static void
|
||||
test_openssh_sign_libssh_verify_combo(struct sshsig_st *test_state,
|
||||
const struct key_hash_combo *combo)
|
||||
{
|
||||
char cmd[1024];
|
||||
char *openssh_sig = NULL;
|
||||
ssh_key verify_key = NULL;
|
||||
FILE *fp = NULL;
|
||||
int rc;
|
||||
|
||||
if (combo->key_type == SSH_KEYTYPE_ED25519 && ssh_fips_mode()) {
|
||||
skip();
|
||||
}
|
||||
|
||||
fp = fopen("test_message.txt", "wb");
|
||||
assert_non_null(fp);
|
||||
/* Write binary data including null byte */
|
||||
rc = fwrite(input, input_len, 1, fp);
|
||||
assert_return_code(rc, errno);
|
||||
rc = fclose(fp);
|
||||
assert_return_code(rc, errno);
|
||||
|
||||
snprintf(cmd,
|
||||
sizeof(cmd),
|
||||
"%s -Y sign -f test_%s -n %s test_message.txt",
|
||||
test_state->ssh_keygen_path,
|
||||
combo->key_name,
|
||||
test_namespace);
|
||||
rc = run_openssh_command(cmd);
|
||||
|
||||
assert_int_equal(rc, 0);
|
||||
openssh_sig = torture_pki_read_file("test_message.txt.sig");
|
||||
assert_non_null(openssh_sig);
|
||||
|
||||
rc = sshsig_verify(input,
|
||||
input_len,
|
||||
openssh_sig,
|
||||
test_namespace,
|
||||
&verify_key);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
assert_non_null(verify_key);
|
||||
|
||||
ssh_key_free(verify_key);
|
||||
free(openssh_sig);
|
||||
rc = unlink("test_message.txt.sig");
|
||||
assert_return_code(rc, errno);
|
||||
rc = unlink("test_message.txt");
|
||||
assert_return_code(rc, errno);
|
||||
}
|
||||
|
||||
static void
|
||||
test_libssh_sign_openssh_verify_combo(struct sshsig_st *test_state,
|
||||
const struct key_hash_combo *combo)
|
||||
{
|
||||
char *libssh_sig = NULL;
|
||||
char cmd[1024];
|
||||
FILE *fp = NULL;
|
||||
int rc;
|
||||
char *pubkey_b64 = NULL;
|
||||
ssh_key test_key = NULL;
|
||||
|
||||
if (combo->key_type == SSH_KEYTYPE_ED25519 && ssh_fips_mode()) {
|
||||
skip();
|
||||
}
|
||||
|
||||
printf("Testing key type: %s\n", combo->key_name);
|
||||
test_key = get_test_key(test_state, combo->key_type);
|
||||
assert_non_null(test_key);
|
||||
|
||||
fp = fopen("test_message.txt", "wb");
|
||||
assert_non_null(fp);
|
||||
/* Write binary data including null byte */
|
||||
rc = fwrite(input, input_len, 1, fp);
|
||||
assert_return_code(rc, errno);
|
||||
rc = fclose(fp);
|
||||
assert_return_code(rc, errno);
|
||||
|
||||
rc = sshsig_sign(input,
|
||||
input_len,
|
||||
test_key,
|
||||
test_namespace,
|
||||
combo->hash_alg,
|
||||
&libssh_sig);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
assert_non_null(libssh_sig);
|
||||
|
||||
fp = fopen("test_message.txt.sig", "w");
|
||||
assert_non_null(fp);
|
||||
rc = fputs(libssh_sig, fp);
|
||||
assert_return_code(rc, errno);
|
||||
rc = fclose(fp);
|
||||
assert_return_code(rc, errno);
|
||||
|
||||
rc = ssh_pki_export_pubkey_base64(test_key, &pubkey_b64);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
|
||||
fp = fopen("allowed_signers", "w");
|
||||
assert_non_null(fp);
|
||||
rc = fprintf(fp, "test %s %s\n", test_key->type_c, pubkey_b64);
|
||||
assert_return_code(rc, errno);
|
||||
rc = fclose(fp);
|
||||
assert_return_code(rc, errno);
|
||||
|
||||
snprintf(cmd,
|
||||
sizeof(cmd),
|
||||
"%s -Y verify -f allowed_signers -I test -n %s -s "
|
||||
"test_message.txt.sig < test_message.txt",
|
||||
test_state->ssh_keygen_path,
|
||||
test_namespace);
|
||||
rc = run_openssh_command(cmd);
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
free(libssh_sig);
|
||||
free(pubkey_b64);
|
||||
rc = unlink("test_message.txt.sig");
|
||||
assert_return_code(rc, errno);
|
||||
rc = unlink("allowed_signers");
|
||||
assert_return_code(rc, errno);
|
||||
rc = unlink("test_message.txt");
|
||||
assert_return_code(rc, errno);
|
||||
}
|
||||
|
||||
static void torture_sshsig_libssh_all_combinations(void **state)
|
||||
{
|
||||
struct sshsig_st *test_state = *state;
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < test_state->num_combinations; i++) {
|
||||
test_libssh_sign_verify_combo(test_state,
|
||||
&test_state->test_combinations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void torture_sshsig_openssh_libssh_all_combinations(void **state)
|
||||
{
|
||||
struct sshsig_st *test_state = *state;
|
||||
size_t i;
|
||||
|
||||
if (test_state->ssh_keygen_path == NULL) {
|
||||
skip();
|
||||
}
|
||||
|
||||
for (i = 0; i < test_state->num_combinations; i++) {
|
||||
test_openssh_sign_libssh_verify_combo(
|
||||
test_state,
|
||||
&test_state->test_combinations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void torture_sshsig_libssh_openssh_all_combinations(void **state)
|
||||
{
|
||||
struct sshsig_st *test_state = *state;
|
||||
size_t i;
|
||||
|
||||
if (test_state->ssh_keygen_path == NULL) {
|
||||
skip();
|
||||
}
|
||||
|
||||
for (i = 0; i < test_state->num_combinations; i++) {
|
||||
test_libssh_sign_openssh_verify_combo(
|
||||
test_state,
|
||||
&test_state->test_combinations[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void torture_sshsig_error_cases_all_combinations(void **state)
|
||||
{
|
||||
struct sshsig_st *test_state = *state;
|
||||
char *signature = NULL;
|
||||
ssh_key verify_key = NULL;
|
||||
int rc;
|
||||
size_t i;
|
||||
char tampered_data[] = "Tampered\0data";
|
||||
|
||||
for (i = 0; i < test_state->num_combinations; i++) {
|
||||
const struct key_hash_combo *combo = &test_state->test_combinations[i];
|
||||
ssh_key test_key = NULL;
|
||||
|
||||
if (combo->key_type == SSH_KEYTYPE_ED25519 && ssh_fips_mode()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
test_key = get_test_key(test_state, combo->key_type);
|
||||
assert_non_null(test_key);
|
||||
|
||||
rc = sshsig_sign(input,
|
||||
input_len,
|
||||
test_key,
|
||||
"", /* Test empty string namespace */
|
||||
combo->hash_alg,
|
||||
&signature);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
assert_null(signature);
|
||||
|
||||
rc = sshsig_sign(input,
|
||||
input_len,
|
||||
test_key,
|
||||
test_namespace,
|
||||
combo->hash_alg,
|
||||
&signature);
|
||||
assert_int_equal(rc, SSH_OK);
|
||||
assert_non_null(signature);
|
||||
|
||||
rc = sshsig_verify(input,
|
||||
input_len,
|
||||
signature,
|
||||
"wrong_namespace",
|
||||
&verify_key);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
assert_null(verify_key);
|
||||
|
||||
rc = sshsig_verify(input,
|
||||
input_len,
|
||||
signature,
|
||||
"", /* Test empty string namespace */
|
||||
&verify_key);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
assert_null(verify_key);
|
||||
|
||||
rc = sshsig_verify(tampered_data,
|
||||
sizeof(tampered_data) - 1,
|
||||
signature,
|
||||
test_namespace,
|
||||
&verify_key);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
assert_null(verify_key);
|
||||
|
||||
free(signature);
|
||||
signature = NULL;
|
||||
}
|
||||
|
||||
/* Test invalid hash algorithm */
|
||||
rc = sshsig_sign(input,
|
||||
input_len,
|
||||
test_state->rsa_key,
|
||||
test_namespace,
|
||||
2,
|
||||
&signature);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
|
||||
/* Test NULL parameters */
|
||||
rc = sshsig_sign(input,
|
||||
input_len,
|
||||
NULL,
|
||||
test_namespace,
|
||||
SSHSIG_DIGEST_SHA2_256,
|
||||
&signature);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
|
||||
rc =
|
||||
sshsig_verify(input, input_len, "invalid", test_namespace, &verify_key);
|
||||
assert_int_equal(rc, SSH_ERROR);
|
||||
}
|
||||
|
||||
int torture_run_tests(void)
|
||||
{
|
||||
int rc;
|
||||
struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(torture_pki_sshsig_armor_dearmor),
|
||||
cmocka_unit_test(torture_pki_sshsig_armor_dearmor_invalid),
|
||||
/* Comprehensive combination tests */
|
||||
cmocka_unit_test_setup_teardown(torture_sshsig_libssh_all_combinations,
|
||||
setup_sshsig_compat,
|
||||
teardown_sshsig_compat),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
torture_sshsig_openssh_libssh_all_combinations,
|
||||
setup_sshsig_compat,
|
||||
teardown_sshsig_compat),
|
||||
cmocka_unit_test_setup_teardown(
|
||||
torture_sshsig_libssh_openssh_all_combinations,
|
||||
setup_sshsig_compat,
|
||||
teardown_sshsig_compat),
|
||||
|
||||
/* Comprehensive error case testing */
|
||||
cmocka_unit_test_setup_teardown(
|
||||
torture_sshsig_error_cases_all_combinations,
|
||||
setup_sshsig_compat,
|
||||
teardown_sshsig_compat),
|
||||
};
|
||||
|
||||
ssh_init();
|
||||
torture_filter_tests(tests);
|
||||
rc = cmocka_run_group_tests(tests, NULL, NULL);
|
||||
ssh_finalize();
|
||||
|
||||
return rc;
|
||||
}
|
||||
Reference in New Issue
Block a user