1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 17:38:00 +00:00

Move the code for cgtest into cgtest.c.

I don't really know why it was still in cmdgen.c at all. There's no
reason it shouldn't live in its own source file, and keep cmdgen.c for
the actual code of the key generation program!
This commit is contained in:
Simon Tatham 2020-02-02 14:15:56 +00:00
parent 7599a57a33
commit be30aac153
2 changed files with 755 additions and 752 deletions

756
cgtest.c
View File

@ -2,5 +2,759 @@
* cgtest.c: stub file to compile cmdgen.c in self-test mode
*/
#define TEST_CMDGEN
/*
* Before we #include cmdgen.c, we override some function names for
* test purposes. We do this via #define, so that when we link against
* modules containing the original versions, we don't get a link-time
* symbol clash:
*
* - Calls to get_random_data() are replaced with the diagnostic
* function below, in order to avoid depleting the test system's
* /dev/random unnecessarily.
*
* - Calls to console_get_userpass_input() are replaced with the
* diagnostic function below, so that I can run tests in an
* automated manner and provide their interactive passphrase
* inputs.
*
* - The main() defined by cmdgen.c is renamed to cmdgen_main(); in
* this file I define another main() which calls the former
* repeatedly to run tests.
*/
#define get_random_data get_random_data_diagnostic
#define console_get_userpass_input console_get_userpass_input_diagnostic
#define main cmdgen_main
#include "cmdgen.c"
#undef get_random_data
#undef console_get_userpass_input
#undef main
static bool cgtest_verbose = false;
/*
* Define the special versions of get_random_data and
* console_get_userpass_input that we need for this test rig.
*/
char *get_random_data_diagnostic(int len, const char *device)
{
char *buf = snewn(len, char);
memset(buf, 'x', len);
return buf;
}
static int nprompts, promptsgot;
static const char *prompts[3];
int console_get_userpass_input_diagnostic(prompts_t *p)
{
size_t i;
int ret = 1;
for (i = 0; i < p->n_prompts; i++) {
if (promptsgot < nprompts) {
prompt_set_result(p->prompts[i], prompts[promptsgot++]);
if (cgtest_verbose)
printf(" prompt \"%s\": response \"%s\"\n",
p->prompts[i]->prompt, p->prompts[i]->result->s);
} else {
promptsgot++; /* track number of requests anyway */
ret = 0;
if (cgtest_verbose)
printf(" prompt \"%s\": no response preloaded\n",
p->prompts[i]->prompt);
}
}
return ret;
}
#include <stdarg.h>
static int passes, fails;
void setup_passphrases(char *first, ...)
{
va_list ap;
char *next;
nprompts = 0;
if (first) {
prompts[nprompts++] = first;
va_start(ap, first);
while ((next = va_arg(ap, char *)) != NULL) {
assert(nprompts < lenof(prompts));
prompts[nprompts++] = next;
}
va_end(ap);
}
}
void test(int retval, ...)
{
va_list ap;
int i, argc, ret;
char **argv;
argc = 0;
va_start(ap, retval);
while (va_arg(ap, char *) != NULL)
argc++;
va_end(ap);
argv = snewn(argc+1, char *);
va_start(ap, retval);
for (i = 0; i <= argc; i++)
argv[i] = va_arg(ap, char *);
va_end(ap);
promptsgot = 0;
if (cgtest_verbose) {
printf("run:");
for (int i = 0; i < argc; i++) {
static const char okchars[] =
"0123456789abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ%+,-./:=[]^_";
const char *arg = argv[i];
printf(" ");
if (arg[strspn(arg, okchars)]) {
printf("'");
for (const char *c = argv[i]; *c; c++) {
if (*c == '\'') {
printf("'\\''");
} else {
putchar(*c);
}
}
printf("'");
} else {
fputs(arg, stdout);
}
}
printf("\n");
}
ret = cmdgen_main(argc, argv);
random_clear();
if (ret != retval) {
printf("FAILED retval (exp %d got %d):", retval, ret);
for (i = 0; i < argc; i++)
printf(" %s", argv[i]);
printf("\n");
fails++;
} else if (promptsgot != nprompts) {
printf("FAILED nprompts (exp %d got %d):", nprompts, promptsgot);
for (i = 0; i < argc; i++)
printf(" %s", argv[i]);
printf("\n");
fails++;
} else {
passes++;
}
sfree(argv);
}
PRINTF_LIKE(3, 4) void filecmp(char *file1, char *file2, char *fmt, ...)
{
/*
* Ideally I should do file comparison myself, to maximise the
* portability of this test suite once this application begins
* running on non-Unix platforms. For the moment, though,
* calling Unix diff is perfectly adequate.
*/
char *buf;
int ret;
buf = dupprintf("diff -q '%s' '%s'", file1, file2);
ret = system(buf);
sfree(buf);
if (ret) {
va_list ap;
printf("FAILED diff (ret=%d): ", ret);
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
printf("\n");
fails++;
} else
passes++;
}
/*
* General-purpose flags word
*/
#define CGT_FLAGS(X) \
X(CGT_TYPE_KNOWN_EARLY) \
X(CGT_OPENSSH) \
X(CGT_SSHCOM) \
X(CGT_SSH_KEYGEN) \
X(CGT_ED25519) \
/* end of list */
#define FLAG_SHIFTS(name) name ## _shift,
enum { CGT_FLAGS(FLAG_SHIFTS) CGT_dummy_shift };
#define FLAG_VALUES(name) name = 1 << name ## _shift,
enum { CGT_FLAGS(FLAG_VALUES) CGT_dummy_flag };
char *cleanup_fp(char *s, unsigned flags)
{
ptrlen pl = ptrlen_from_asciz(s);
static const char separators[] = " \n\t";
/* Skip initial key type word if we find one */
if (ptrlen_startswith(pl, PTRLEN_LITERAL("ssh-"), NULL) ||
ptrlen_startswith(pl, PTRLEN_LITERAL("ecdsa-"), NULL))
ptrlen_get_word(&pl, separators);
/* Expect two words giving the key length and the hash */
ptrlen bits = ptrlen_get_word(&pl, separators);
ptrlen hash = ptrlen_get_word(&pl, separators);
if (flags & CGT_SSH_KEYGEN) {
/* Strip "MD5:" prefix if it's present, and do nothing if it isn't */
ptrlen_startswith(hash, PTRLEN_LITERAL("MD5:"), &hash);
if (flags & CGT_ED25519) {
/* OpenSSH ssh-keygen lists ed25519 keys as 256 bits, not 255 */
if (ptrlen_eq_string(bits, "256"))
bits = PTRLEN_LITERAL("255");
}
}
return dupprintf("%.*s %.*s", PTRLEN_PRINTF(bits), PTRLEN_PRINTF(hash));
}
char *get_line(char *filename)
{
FILE *fp;
char *line;
fp = fopen(filename, "r");
if (!fp)
return NULL;
line = fgetline(fp);
fclose(fp);
return line;
}
char *get_fp(char *filename, unsigned flags)
{
char *orig = get_line(filename);
if (!orig)
return NULL;
char *toret = cleanup_fp(orig, flags);
sfree(orig);
return toret;
}
PRINTF_LIKE(3, 4) void check_fp(char *filename, char *fp, char *fmt, ...)
{
char *newfp;
if (!fp)
return;
newfp = get_fp(filename, 0);
if (!strcmp(fp, newfp)) {
passes++;
} else {
va_list ap;
printf("FAILED check_fp ['%s' != '%s']: ", newfp, fp);
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
printf("\n");
fails++;
}
sfree(newfp);
}
static const struct cgtest_keytype {
const char *name;
unsigned flags;
} cgtest_keytypes[] = {
{ "rsa1", CGT_TYPE_KNOWN_EARLY },
{ "dsa", CGT_OPENSSH | CGT_SSHCOM },
{ "rsa", CGT_OPENSSH | CGT_SSHCOM },
{ "ecdsa", CGT_OPENSSH },
{ "ed25519", CGT_OPENSSH | CGT_ED25519 },
};
int main(int argc, char **argv)
{
int i;
int active[lenof(cgtest_keytypes)], active_value;
bool remove_files = true;
active_value = 0;
for (i = 0; i < lenof(cgtest_keytypes); i++)
active[i] = active_value;
while (--argc > 0) {
ptrlen arg = ptrlen_from_asciz(*++argv);
if (ptrlen_eq_string(arg, "-v") ||
ptrlen_eq_string(arg, "--verbose")) {
cgtest_verbose = true;
} else if (ptrlen_eq_string(arg, "--keep")) {
remove_files = false;
} else if (ptrlen_eq_string(arg, "--help")) {
printf("usage: cgtest [options] [key types]\n");
printf("options: -v, --verbose "
"print more output during tests\n");
printf(" --keep "
"do not delete the temporary output files\n");
printf(" --help "
"display this help text\n");
printf("key types: ");
for (i = 0; i < lenof(cgtest_keytypes); i++)
printf("%s%s", i ? ", " : "", cgtest_keytypes[i].name);
printf("\n");
return 0;
} else if (!ptrlen_startswith(arg, PTRLEN_LITERAL("-"), NULL)) {
for (i = 0; i < lenof(cgtest_keytypes); i++)
if (ptrlen_eq_string(arg, cgtest_keytypes[i].name))
break;
if (i == lenof(cgtest_keytypes)) {
fprintf(stderr, "cgtest: unrecognised key type '%.*s'\n",
PTRLEN_PRINTF(arg));
return 1;
}
active_value = 1; /* disables all keys not explicitly enabled */
active[i] = active_value;
} else {
fprintf(stderr, "cgtest: unrecognised option '%.*s'\n",
PTRLEN_PRINTF(arg));
return 1;
}
}
passes = fails = 0;
for (i = 0; i < lenof(cgtest_keytypes); i++) {
if (active[i] != active_value)
continue;
const struct cgtest_keytype *keytype = &cgtest_keytypes[i];
bool supports_openssh = keytype->flags & CGT_OPENSSH;
bool supports_sshcom = keytype->flags & CGT_SSHCOM;
bool type_known_early = keytype->flags & CGT_TYPE_KNOWN_EARLY;
char filename[128], osfilename[128], scfilename[128];
char pubfilename[128], tmpfilename1[128], tmpfilename2[128];
char *fp = NULL;
sprintf(filename, "test-%s.ppk", keytype->name);
sprintf(pubfilename, "test-%s.pub", keytype->name);
sprintf(osfilename, "test-%s.os", keytype->name);
sprintf(scfilename, "test-%s.sc", keytype->name);
sprintf(tmpfilename1, "test-%s.tmp1", keytype->name);
sprintf(tmpfilename2, "test-%s.tmp2", keytype->name);
/*
* Create an encrypted key.
*/
setup_passphrases("sponge", "sponge", NULL);
test(0, "puttygen", "-t", keytype->name, "-o", filename, NULL);
/*
* List the public key in OpenSSH format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-L", filename, "-o", pubfilename, NULL);
{
char *cmdbuf;
fp = NULL;
cmdbuf = dupprintf("ssh-keygen -E md5 -l -f '%s' > '%s'",
pubfilename, tmpfilename1);
if (cgtest_verbose)
printf("OpenSSH fp check: %s\n", cmdbuf);
if (system(cmdbuf) ||
(fp = get_fp(tmpfilename1,
CGT_SSH_KEYGEN | keytype->flags)) == NULL) {
printf("UNABLE to test fingerprint matching against "
"OpenSSH\n");
}
sfree(cmdbuf);
if (fp && cgtest_verbose) {
char *line = get_line(tmpfilename1);
printf("OpenSSH fp: %s\n", line);
printf("Cleaned up: %s\n", fp);
sfree(line);
}
}
/*
* List the public key in IETF/ssh.com format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-p", filename, NULL);
/*
* List the fingerprint of the key.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-l", filename, "-o", tmpfilename1, NULL);
if (!fp) {
/*
* If we can't test fingerprints against OpenSSH, we
* can at the very least test equality of all the
* fingerprints we generate of this key throughout
* testing.
*/
fp = get_fp(tmpfilename1, 0);
} else {
check_fp(tmpfilename1, fp, "%s initial fp", keytype->name);
}
/*
* Change the comment of the key; this _does_ require a
* passphrase owing to the tamperproofing.
*
* NOTE: In SSH-1, this only requires a passphrase because
* of inadequacies of the loading and saving mechanisms. In
* _principle_, it should be perfectly possible to modify
* the comment on an SSH-1 key without requiring a
* passphrase; the only reason I can't do it is because my
* loading and saving mechanisms don't include a method of
* loading all the key data without also trying to decrypt
* the private section.
*
* I don't consider this to be a problem worth solving,
* because (a) to fix it would probably end up bloating
* PuTTY proper, and (b) SSH-1 is on the way out anyway so
* it shouldn't be highly significant. If it seriously
* bothers anyone then perhaps I _might_ be persuadable.
*/
setup_passphrases("sponge", NULL);
test(0, "puttygen", "-C", "new-comment", filename, NULL);
/*
* Change the passphrase to nothing.
*/
setup_passphrases("sponge", "", "", NULL);
test(0, "puttygen", "-P", filename, NULL);
/*
* Change the comment of the key again; this time we expect no
* passphrase to be required.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-C", "new-comment-2", filename, NULL);
/*
* Export the private key into OpenSSH format; no passphrase
* should be required since the key is currently unencrypted.
*/
setup_passphrases(NULL);
test(supports_openssh ? 0 : 1,
"puttygen", "-O", "private-openssh", "-o", osfilename,
filename, NULL);
if (supports_openssh) {
/*
* List the fingerprint of the OpenSSH-formatted key.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-l", osfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s openssh clear fp", keytype->name);
/*
* List the public half of the OpenSSH-formatted key in
* OpenSSH format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-L", osfilename, NULL);
/*
* List the public half of the OpenSSH-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-p", osfilename, NULL);
}
/*
* Export the private key into ssh.com format; no passphrase
* should be required since the key is currently unencrypted.
*/
setup_passphrases(NULL);
test(supports_sshcom ? 0 : 1,
"puttygen", "-O", "private-sshcom",
"-o", scfilename, filename, NULL);
if (supports_sshcom) {
/*
* List the fingerprint of the ssh.com-formatted key.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-l", scfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s ssh.com clear fp", keytype->name);
/*
* List the public half of the ssh.com-formatted key in
* OpenSSH format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-L", scfilename, NULL);
/*
* List the public half of the ssh.com-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-p", scfilename, NULL);
}
if (supports_openssh && supports_sshcom) {
/*
* Convert from OpenSSH into ssh.com.
*/
setup_passphrases(NULL);
test(0, "puttygen", osfilename, "-o", tmpfilename1,
"-O", "private-sshcom", NULL);
/*
* Convert from ssh.com back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases(NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->o->s->p clear %s", keytype->name);
/*
* Convert from ssh.com to OpenSSH.
*/
setup_passphrases(NULL);
test(0, "puttygen", scfilename, "-o", tmpfilename1,
"-O", "private-openssh", NULL);
/*
* Convert from OpenSSH back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases(NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->s->o->p clear %s", keytype->name);
/*
* Finally, do a round-trip conversion between PuTTY
* and ssh.com without involving OpenSSH, to test that
* the key comment is preserved in that case.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-O", "private-sshcom", "-o", tmpfilename1,
filename, NULL);
setup_passphrases(NULL);
test(0, "puttygen", tmpfilename1, "-o", tmpfilename2, NULL);
filecmp(filename, tmpfilename2,
"p->s->p clear %s", keytype->name);
}
/*
* Check that mismatched passphrases cause an error.
*/
setup_passphrases("sponge2", "sponge3", NULL);
test(1, "puttygen", "-P", filename, NULL);
/*
* Put a passphrase back on.
*/
setup_passphrases("sponge2", "sponge2", NULL);
test(0, "puttygen", "-P", filename, NULL);
/*
* Export the private key into OpenSSH format, this time
* while encrypted.
*/
if (!supports_openssh && type_known_early) {
/* We'll know far enough in advance that this combination
* is going to fail that we never ask for the passphrase */
setup_passphrases(NULL);
} else {
setup_passphrases("sponge2", NULL);
}
test(supports_openssh ? 0 : 1,
"puttygen", "-O", "private-openssh", "-o", osfilename,
filename, NULL);
if (supports_openssh) {
/*
* List the fingerprint of the OpenSSH-formatted key.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-l", osfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s openssh encrypted fp",
keytype->name);
/*
* List the public half of the OpenSSH-formatted key in
* OpenSSH format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-L", osfilename, NULL);
/*
* List the public half of the OpenSSH-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-p", osfilename, NULL);
}
/*
* Export the private key into ssh.com format, this time
* while encrypted. For RSA1 keys, this should give an
* error.
*/
if (!supports_sshcom && type_known_early) {
/* We'll know far enough in advance that this combination
* is going to fail that we never ask for the passphrase */
setup_passphrases(NULL);
} else {
setup_passphrases("sponge2", NULL);
}
test(supports_sshcom ? 0 : 1,
"puttygen", "-O", "private-sshcom", "-o", scfilename,
filename, NULL);
if (supports_sshcom) {
/*
* List the fingerprint of the ssh.com-formatted key.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-l", scfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s ssh.com encrypted fp",
keytype->name);
/*
* List the public half of the ssh.com-formatted key in
* OpenSSH format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-L", scfilename, NULL);
/*
* List the public half of the ssh.com-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-p", scfilename, NULL);
}
if (supports_openssh && supports_sshcom) {
/*
* Convert from OpenSSH into ssh.com.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", osfilename, "-o", tmpfilename1,
"-O", "private-sshcom", NULL);
/*
* Convert from ssh.com back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->o->s->p encrypted %s", keytype->name);
/*
* Convert from ssh.com to OpenSSH.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", scfilename, "-o", tmpfilename1,
"-O", "private-openssh", NULL);
/*
* Convert from OpenSSH back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->s->o->p encrypted %s", keytype->name);
/*
* Finally, do a round-trip conversion between PuTTY
* and ssh.com without involving OpenSSH, to test that
* the key comment is preserved in that case.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-O", "private-sshcom", "-o", tmpfilename1,
filename, NULL);
setup_passphrases("sponge2", NULL);
test(0, "puttygen", tmpfilename1, "-o", tmpfilename2, NULL);
filecmp(filename, tmpfilename2,
"p->s->p encrypted %s", keytype->name);
}
/*
* Load with the wrong passphrase.
*/
setup_passphrases("sponge8", NULL);
test(1, "puttygen", "-C", "spurious-new-comment", filename, NULL);
/*
* Load a totally bogus file.
*/
setup_passphrases(NULL);
test(1, "puttygen", "-C", "spurious-new-comment", pubfilename, NULL);
sfree(fp);
if (remove_files) {
remove(filename);
remove(pubfilename);
remove(osfilename);
remove(scfilename);
remove(tmpfilename1);
remove(tmpfilename2);
}
}
printf("%d passes, %d fails\n", passes, fails);
return fails == 0 ? 0 : 1;
}

751
cmdgen.c
View File

@ -14,60 +14,6 @@
#include "putty.h"
#include "ssh.h"
#ifdef TEST_CMDGEN
/*
* This section overrides some definitions below for test purposes.
* When compiled with -DTEST_CMDGEN (as cgtest.c will do):
*
* - Calls to get_random_data() are replaced with the diagnostic
* function below (I #define the name so that I can still link
* with the original set of modules without symbol clash), in
* order to avoid depleting the test system's /dev/random
* unnecessarily.
*
* - Calls to console_get_userpass_input() are replaced with the
* diagnostic function below, so that I can run tests in an
* automated manner and provide their interactive passphrase
* inputs.
*
* - main() is renamed to cmdgen_main(); at the bottom of the file
* I define another main() which calls the former repeatedly to
* run tests.
*/
static bool cgtest_verbose = false;
#define get_random_data get_random_data_diagnostic
char *get_random_data(int len, const char *device)
{
char *buf = snewn(len, char);
memset(buf, 'x', len);
return buf;
}
#define console_get_userpass_input console_get_userpass_input_diagnostic
static int nprompts, promptsgot;
static const char *prompts[3];
int console_get_userpass_input(prompts_t *p)
{
size_t i;
int ret = 1;
for (i = 0; i < p->n_prompts; i++) {
if (promptsgot < nprompts) {
prompt_set_result(p->prompts[i], prompts[promptsgot++]);
if (cgtest_verbose)
printf(" prompt \"%s\": response \"%s\"\n",
p->prompts[i]->prompt, p->prompts[i]->result->s);
} else {
promptsgot++; /* track number of requests anyway */
ret = 0;
if (cgtest_verbose)
printf(" prompt \"%s\": no response preloaded\n",
p->prompts[i]->prompt);
}
}
return ret;
}
#define main cmdgen_main
#endif
struct progress {
int phase, current;
};
@ -1133,700 +1079,3 @@ int main(int argc, char **argv)
return exit_status;
}
#ifdef TEST_CMDGEN
#undef main
#include <stdarg.h>
static int passes, fails;
void setup_passphrases(char *first, ...)
{
va_list ap;
char *next;
nprompts = 0;
if (first) {
prompts[nprompts++] = first;
va_start(ap, first);
while ((next = va_arg(ap, char *)) != NULL) {
assert(nprompts < lenof(prompts));
prompts[nprompts++] = next;
}
va_end(ap);
}
}
void test(int retval, ...)
{
va_list ap;
int i, argc, ret;
char **argv;
argc = 0;
va_start(ap, retval);
while (va_arg(ap, char *) != NULL)
argc++;
va_end(ap);
argv = snewn(argc+1, char *);
va_start(ap, retval);
for (i = 0; i <= argc; i++)
argv[i] = va_arg(ap, char *);
va_end(ap);
promptsgot = 0;
if (cgtest_verbose) {
printf("run:");
for (int i = 0; i < argc; i++) {
static const char okchars[] =
"0123456789abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ%+,-./:=[]^_";
const char *arg = argv[i];
printf(" ");
if (arg[strspn(arg, okchars)]) {
printf("'");
for (const char *c = argv[i]; *c; c++) {
if (*c == '\'') {
printf("'\\''");
} else {
putchar(*c);
}
}
printf("'");
} else {
fputs(arg, stdout);
}
}
printf("\n");
}
ret = cmdgen_main(argc, argv);
random_clear();
if (ret != retval) {
printf("FAILED retval (exp %d got %d):", retval, ret);
for (i = 0; i < argc; i++)
printf(" %s", argv[i]);
printf("\n");
fails++;
} else if (promptsgot != nprompts) {
printf("FAILED nprompts (exp %d got %d):", nprompts, promptsgot);
for (i = 0; i < argc; i++)
printf(" %s", argv[i]);
printf("\n");
fails++;
} else {
passes++;
}
sfree(argv);
}
PRINTF_LIKE(3, 4) void filecmp(char *file1, char *file2, char *fmt, ...)
{
/*
* Ideally I should do file comparison myself, to maximise the
* portability of this test suite once this application begins
* running on non-Unix platforms. For the moment, though,
* calling Unix diff is perfectly adequate.
*/
char *buf;
int ret;
buf = dupprintf("diff -q '%s' '%s'", file1, file2);
ret = system(buf);
sfree(buf);
if (ret) {
va_list ap;
printf("FAILED diff (ret=%d): ", ret);
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
printf("\n");
fails++;
} else
passes++;
}
/*
* General-purpose flags word
*/
#define CGT_FLAGS(X) \
X(CGT_TYPE_KNOWN_EARLY) \
X(CGT_OPENSSH) \
X(CGT_SSHCOM) \
X(CGT_SSH_KEYGEN) \
X(CGT_ED25519) \
/* end of list */
#define FLAG_SHIFTS(name) name ## _shift,
enum { CGT_FLAGS(FLAG_SHIFTS) CGT_dummy_shift };
#define FLAG_VALUES(name) name = 1 << name ## _shift,
enum { CGT_FLAGS(FLAG_VALUES) CGT_dummy_flag };
char *cleanup_fp(char *s, unsigned flags)
{
ptrlen pl = ptrlen_from_asciz(s);
static const char separators[] = " \n\t";
/* Skip initial key type word if we find one */
if (ptrlen_startswith(pl, PTRLEN_LITERAL("ssh-"), NULL) ||
ptrlen_startswith(pl, PTRLEN_LITERAL("ecdsa-"), NULL))
ptrlen_get_word(&pl, separators);
/* Expect two words giving the key length and the hash */
ptrlen bits = ptrlen_get_word(&pl, separators);
ptrlen hash = ptrlen_get_word(&pl, separators);
if (flags & CGT_SSH_KEYGEN) {
/* Strip "MD5:" prefix if it's present, and do nothing if it isn't */
ptrlen_startswith(hash, PTRLEN_LITERAL("MD5:"), &hash);
if (flags & CGT_ED25519) {
/* OpenSSH ssh-keygen lists ed25519 keys as 256 bits, not 255 */
if (ptrlen_eq_string(bits, "256"))
bits = PTRLEN_LITERAL("255");
}
}
return dupprintf("%.*s %.*s", PTRLEN_PRINTF(bits), PTRLEN_PRINTF(hash));
}
char *get_line(char *filename)
{
FILE *fp;
char *line;
fp = fopen(filename, "r");
if (!fp)
return NULL;
line = fgetline(fp);
fclose(fp);
return line;
}
char *get_fp(char *filename, unsigned flags)
{
char *orig = get_line(filename);
if (!orig)
return NULL;
char *toret = cleanup_fp(orig, flags);
sfree(orig);
return toret;
}
PRINTF_LIKE(3, 4) void check_fp(char *filename, char *fp, char *fmt, ...)
{
char *newfp;
if (!fp)
return;
newfp = get_fp(filename, 0);
if (!strcmp(fp, newfp)) {
passes++;
} else {
va_list ap;
printf("FAILED check_fp ['%s' != '%s']: ", newfp, fp);
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
printf("\n");
fails++;
}
sfree(newfp);
}
static const struct cgtest_keytype {
const char *name;
unsigned flags;
} cgtest_keytypes[] = {
{ "rsa1", CGT_TYPE_KNOWN_EARLY },
{ "dsa", CGT_OPENSSH | CGT_SSHCOM },
{ "rsa", CGT_OPENSSH | CGT_SSHCOM },
{ "ecdsa", CGT_OPENSSH },
{ "ed25519", CGT_OPENSSH | CGT_ED25519 },
};
int main(int argc, char **argv)
{
int i;
int active[lenof(cgtest_keytypes)], active_value;
bool remove_files = true;
active_value = 0;
for (i = 0; i < lenof(cgtest_keytypes); i++)
active[i] = active_value;
while (--argc > 0) {
ptrlen arg = ptrlen_from_asciz(*++argv);
if (ptrlen_eq_string(arg, "-v") ||
ptrlen_eq_string(arg, "--verbose")) {
cgtest_verbose = true;
} else if (ptrlen_eq_string(arg, "--keep")) {
remove_files = false;
} else if (ptrlen_eq_string(arg, "--help")) {
printf("usage: cgtest [options] [key types]\n");
printf("options: -v, --verbose "
"print more output during tests\n");
printf(" --keep "
"do not delete the temporary output files\n");
printf(" --help "
"display this help text\n");
printf("key types: ");
for (i = 0; i < lenof(cgtest_keytypes); i++)
printf("%s%s", i ? ", " : "", cgtest_keytypes[i].name);
printf("\n");
return 0;
} else if (!ptrlen_startswith(arg, PTRLEN_LITERAL("-"), NULL)) {
for (i = 0; i < lenof(cgtest_keytypes); i++)
if (ptrlen_eq_string(arg, cgtest_keytypes[i].name))
break;
if (i == lenof(cgtest_keytypes)) {
fprintf(stderr, "cgtest: unrecognised key type '%.*s'\n",
PTRLEN_PRINTF(arg));
return 1;
}
active_value = 1; /* disables all keys not explicitly enabled */
active[i] = active_value;
} else {
fprintf(stderr, "cgtest: unrecognised option '%.*s'\n",
PTRLEN_PRINTF(arg));
return 1;
}
}
passes = fails = 0;
for (i = 0; i < lenof(cgtest_keytypes); i++) {
if (active[i] != active_value)
continue;
const struct cgtest_keytype *keytype = &cgtest_keytypes[i];
bool supports_openssh = keytype->flags & CGT_OPENSSH;
bool supports_sshcom = keytype->flags & CGT_SSHCOM;
bool type_known_early = keytype->flags & CGT_TYPE_KNOWN_EARLY;
char filename[128], osfilename[128], scfilename[128];
char pubfilename[128], tmpfilename1[128], tmpfilename2[128];
char *fp = NULL;
sprintf(filename, "test-%s.ppk", keytype->name);
sprintf(pubfilename, "test-%s.pub", keytype->name);
sprintf(osfilename, "test-%s.os", keytype->name);
sprintf(scfilename, "test-%s.sc", keytype->name);
sprintf(tmpfilename1, "test-%s.tmp1", keytype->name);
sprintf(tmpfilename2, "test-%s.tmp2", keytype->name);
/*
* Create an encrypted key.
*/
setup_passphrases("sponge", "sponge", NULL);
test(0, "puttygen", "-t", keytype->name, "-o", filename, NULL);
/*
* List the public key in OpenSSH format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-L", filename, "-o", pubfilename, NULL);
{
char *cmdbuf;
fp = NULL;
cmdbuf = dupprintf("ssh-keygen -E md5 -l -f '%s' > '%s'",
pubfilename, tmpfilename1);
if (cgtest_verbose)
printf("OpenSSH fp check: %s\n", cmdbuf);
if (system(cmdbuf) ||
(fp = get_fp(tmpfilename1,
CGT_SSH_KEYGEN | keytype->flags)) == NULL) {
printf("UNABLE to test fingerprint matching against "
"OpenSSH\n");
}
sfree(cmdbuf);
if (fp && cgtest_verbose) {
char *line = get_line(tmpfilename1);
printf("OpenSSH fp: %s\n", line);
printf("Cleaned up: %s\n", fp);
sfree(line);
}
}
/*
* List the public key in IETF/ssh.com format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-p", filename, NULL);
/*
* List the fingerprint of the key.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-l", filename, "-o", tmpfilename1, NULL);
if (!fp) {
/*
* If we can't test fingerprints against OpenSSH, we
* can at the very least test equality of all the
* fingerprints we generate of this key throughout
* testing.
*/
fp = get_fp(tmpfilename1, 0);
} else {
check_fp(tmpfilename1, fp, "%s initial fp", keytype->name);
}
/*
* Change the comment of the key; this _does_ require a
* passphrase owing to the tamperproofing.
*
* NOTE: In SSH-1, this only requires a passphrase because
* of inadequacies of the loading and saving mechanisms. In
* _principle_, it should be perfectly possible to modify
* the comment on an SSH-1 key without requiring a
* passphrase; the only reason I can't do it is because my
* loading and saving mechanisms don't include a method of
* loading all the key data without also trying to decrypt
* the private section.
*
* I don't consider this to be a problem worth solving,
* because (a) to fix it would probably end up bloating
* PuTTY proper, and (b) SSH-1 is on the way out anyway so
* it shouldn't be highly significant. If it seriously
* bothers anyone then perhaps I _might_ be persuadable.
*/
setup_passphrases("sponge", NULL);
test(0, "puttygen", "-C", "new-comment", filename, NULL);
/*
* Change the passphrase to nothing.
*/
setup_passphrases("sponge", "", "", NULL);
test(0, "puttygen", "-P", filename, NULL);
/*
* Change the comment of the key again; this time we expect no
* passphrase to be required.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-C", "new-comment-2", filename, NULL);
/*
* Export the private key into OpenSSH format; no passphrase
* should be required since the key is currently unencrypted.
*/
setup_passphrases(NULL);
test(supports_openssh ? 0 : 1,
"puttygen", "-O", "private-openssh", "-o", osfilename,
filename, NULL);
if (supports_openssh) {
/*
* List the fingerprint of the OpenSSH-formatted key.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-l", osfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s openssh clear fp", keytype->name);
/*
* List the public half of the OpenSSH-formatted key in
* OpenSSH format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-L", osfilename, NULL);
/*
* List the public half of the OpenSSH-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-p", osfilename, NULL);
}
/*
* Export the private key into ssh.com format; no passphrase
* should be required since the key is currently unencrypted.
*/
setup_passphrases(NULL);
test(supports_sshcom ? 0 : 1,
"puttygen", "-O", "private-sshcom",
"-o", scfilename, filename, NULL);
if (supports_sshcom) {
/*
* List the fingerprint of the ssh.com-formatted key.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-l", scfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s ssh.com clear fp", keytype->name);
/*
* List the public half of the ssh.com-formatted key in
* OpenSSH format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-L", scfilename, NULL);
/*
* List the public half of the ssh.com-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-p", scfilename, NULL);
}
if (supports_openssh && supports_sshcom) {
/*
* Convert from OpenSSH into ssh.com.
*/
setup_passphrases(NULL);
test(0, "puttygen", osfilename, "-o", tmpfilename1,
"-O", "private-sshcom", NULL);
/*
* Convert from ssh.com back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases(NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->o->s->p clear %s", keytype->name);
/*
* Convert from ssh.com to OpenSSH.
*/
setup_passphrases(NULL);
test(0, "puttygen", scfilename, "-o", tmpfilename1,
"-O", "private-openssh", NULL);
/*
* Convert from OpenSSH back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases(NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->s->o->p clear %s", keytype->name);
/*
* Finally, do a round-trip conversion between PuTTY
* and ssh.com without involving OpenSSH, to test that
* the key comment is preserved in that case.
*/
setup_passphrases(NULL);
test(0, "puttygen", "-O", "private-sshcom", "-o", tmpfilename1,
filename, NULL);
setup_passphrases(NULL);
test(0, "puttygen", tmpfilename1, "-o", tmpfilename2, NULL);
filecmp(filename, tmpfilename2,
"p->s->p clear %s", keytype->name);
}
/*
* Check that mismatched passphrases cause an error.
*/
setup_passphrases("sponge2", "sponge3", NULL);
test(1, "puttygen", "-P", filename, NULL);
/*
* Put a passphrase back on.
*/
setup_passphrases("sponge2", "sponge2", NULL);
test(0, "puttygen", "-P", filename, NULL);
/*
* Export the private key into OpenSSH format, this time
* while encrypted.
*/
if (!supports_openssh && type_known_early) {
/* We'll know far enough in advance that this combination
* is going to fail that we never ask for the passphrase */
setup_passphrases(NULL);
} else {
setup_passphrases("sponge2", NULL);
}
test(supports_openssh ? 0 : 1,
"puttygen", "-O", "private-openssh", "-o", osfilename,
filename, NULL);
if (supports_openssh) {
/*
* List the fingerprint of the OpenSSH-formatted key.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-l", osfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s openssh encrypted fp",
keytype->name);
/*
* List the public half of the OpenSSH-formatted key in
* OpenSSH format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-L", osfilename, NULL);
/*
* List the public half of the OpenSSH-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-p", osfilename, NULL);
}
/*
* Export the private key into ssh.com format, this time
* while encrypted. For RSA1 keys, this should give an
* error.
*/
if (!supports_sshcom && type_known_early) {
/* We'll know far enough in advance that this combination
* is going to fail that we never ask for the passphrase */
setup_passphrases(NULL);
} else {
setup_passphrases("sponge2", NULL);
}
test(supports_sshcom ? 0 : 1,
"puttygen", "-O", "private-sshcom", "-o", scfilename,
filename, NULL);
if (supports_sshcom) {
/*
* List the fingerprint of the ssh.com-formatted key.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-l", scfilename, "-o", tmpfilename1, NULL);
check_fp(tmpfilename1, fp, "%s ssh.com encrypted fp",
keytype->name);
/*
* List the public half of the ssh.com-formatted key in
* OpenSSH format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-L", scfilename, NULL);
/*
* List the public half of the ssh.com-formatted key in
* IETF/ssh.com format.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-p", scfilename, NULL);
}
if (supports_openssh && supports_sshcom) {
/*
* Convert from OpenSSH into ssh.com.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", osfilename, "-o", tmpfilename1,
"-O", "private-sshcom", NULL);
/*
* Convert from ssh.com back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->o->s->p encrypted %s", keytype->name);
/*
* Convert from ssh.com to OpenSSH.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", scfilename, "-o", tmpfilename1,
"-O", "private-openssh", NULL);
/*
* Convert from OpenSSH back into a PuTTY key,
* supplying the same comment as we had before we
* started to ensure the comparison works.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", tmpfilename1, "-C", "new-comment-2",
"-o", tmpfilename2, NULL);
/*
* See if the PuTTY key thus generated is the same as
* the original.
*/
filecmp(filename, tmpfilename2,
"p->s->o->p encrypted %s", keytype->name);
/*
* Finally, do a round-trip conversion between PuTTY
* and ssh.com without involving OpenSSH, to test that
* the key comment is preserved in that case.
*/
setup_passphrases("sponge2", NULL);
test(0, "puttygen", "-O", "private-sshcom", "-o", tmpfilename1,
filename, NULL);
setup_passphrases("sponge2", NULL);
test(0, "puttygen", tmpfilename1, "-o", tmpfilename2, NULL);
filecmp(filename, tmpfilename2,
"p->s->p encrypted %s", keytype->name);
}
/*
* Load with the wrong passphrase.
*/
setup_passphrases("sponge8", NULL);
test(1, "puttygen", "-C", "spurious-new-comment", filename, NULL);
/*
* Load a totally bogus file.
*/
setup_passphrases(NULL);
test(1, "puttygen", "-C", "spurious-new-comment", pubfilename, NULL);
sfree(fp);
if (remove_files) {
remove(filename);
remove(pubfilename);
remove(osfilename);
remove(scfilename);
remove(tmpfilename1);
remove(tmpfilename2);
}
}
printf("%d passes, %d fails\n", passes, fails);
return fails == 0 ? 0 : 1;
}
#endif