diff --git a/cmdgen.c b/cmdgen.c index 7809c891..adfe027f 100644 --- a/cmdgen.c +++ b/cmdgen.c @@ -163,7 +163,7 @@ void help(void) " private-openssh-new export OpenSSH private key " "(force new file format)\n" " private-sshcom export ssh.com private key\n" - " public standard / ssh.com public key\n" + " public RFC 4716 / ssh.com public key\n" " public-openssh OpenSSH public key\n" " fingerprint output the key fingerprint\n" " -o specify output file\n" @@ -583,28 +583,6 @@ int main(int argc, char **argv) intype = key_type(infilename); switch (intype) { - /* - * It would be nice here to be able to load _public_ - * key files, in any of a number of forms, and (a) - * convert them to other public key types, (b) print - * out their fingerprints. Or, I suppose, for real - * orthogonality, (c) change their comment! - * - * In fact this opens some interesting possibilities. - * Suppose ssh2_userkey_loadpub() were able to load - * public key files as well as extracting the public - * key from private ones. And suppose I did the thing - * I've been wanting to do, where specifying a - * particular private key file for authentication - * causes any _other_ key in the agent to be discarded. - * Then, if you had an agent forwarded to the machine - * you were running Unix PuTTY or Plink on, and you - * needed to specify which of the keys in the agent it - * should use, you could do that by supplying a - * _public_ key file, thus not needing to trust even - * your encrypted private key file to the network. Ooh! - */ - case SSH_KEYTYPE_UNOPENABLE: case SSH_KEYTYPE_UNKNOWN: fprintf(stderr, "puttygen: unable to load file `%s': %s\n", @@ -612,6 +590,7 @@ int main(int argc, char **argv) return 1; case SSH_KEYTYPE_SSH1: + case SSH_KEYTYPE_SSH1_PUBLIC: if (sshver == 2) { fprintf(stderr, "puttygen: conversion from SSH-1 to SSH-2 keys" " not supported\n"); @@ -621,6 +600,8 @@ int main(int argc, char **argv) break; case SSH_KEYTYPE_SSH2: + case SSH_KEYTYPE_SSH2_PUBLIC_RFC4716: + case SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH: case SSH_KEYTYPE_OPENSSH_PEM: case SSH_KEYTYPE_OPENSSH_NEW: case SSH_KEYTYPE_SSHCOM: @@ -692,6 +673,14 @@ int main(int argc, char **argv) else load_encrypted = FALSE; + if (load_encrypted && (intype == SSH_KEYTYPE_SSH1_PUBLIC || + intype == SSH_KEYTYPE_SSH2_PUBLIC_RFC4716 || + intype == SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH)) { + fprintf(stderr, "puttygen: cannot perform this action on a " + "public-key-only input file\n"); + return 1; + } + /* ------------------------------------------------------------------ * Now we're ready to actually do some stuff. */ @@ -818,6 +807,7 @@ int main(int argc, char **argv) int ret; case SSH_KEYTYPE_SSH1: + case SSH_KEYTYPE_SSH1_PUBLIC: ssh1key = snew(struct RSAKey); if (!load_encrypted) { void *vblob; @@ -858,9 +848,12 @@ int main(int argc, char **argv) break; case SSH_KEYTYPE_SSH2: + case SSH_KEYTYPE_SSH2_PUBLIC_RFC4716: + case SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH: if (!load_encrypted) { ssh2blob = ssh2_userkey_loadpub(infilename, &ssh2alg, - &ssh2bloblen, NULL, &error); + &ssh2bloblen, &origcomment, + &error); if (ssh2blob) { ssh2algf = find_pubkey_alg(ssh2alg); if (ssh2algf) diff --git a/ssh.h b/ssh.h index 56b770e0..21ec7870 100644 --- a/ssh.h +++ b/ssh.h @@ -706,7 +706,14 @@ enum { SSH_KEYTYPE_OPENSSH_AUTO, SSH_KEYTYPE_OPENSSH_PEM, SSH_KEYTYPE_OPENSSH_NEW, - SSH_KEYTYPE_SSHCOM + SSH_KEYTYPE_SSHCOM, + /* + * Public-key-only formats, which we still want to be able to read + * for various purposes. + */ + SSH_KEYTYPE_SSH1_PUBLIC, + SSH_KEYTYPE_SSH2_PUBLIC_RFC4716, + SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH }; int key_type(const Filename *filename); char *key_type_to_str(int type); diff --git a/sshpubk.c b/sshpubk.c index 90318b72..fd43725e 100644 --- a/sshpubk.c +++ b/sshpubk.c @@ -21,6 +21,8 @@ (x)=='+' ? 62 : \ (x)=='/' ? 63 : 0 ) +static int key_type_fp(FILE *fp); + static int loadrsakey_main(FILE * fp, struct RSAKey *key, int pub_only, char **commentptr, const char *passphrase, const char **error) @@ -261,6 +263,56 @@ int rsakey_pubblob(const Filename *filename, void **blob, int *bloblen, } fp = NULL; /* loadrsakey_main unconditionally closes fp */ } else { + /* + * Try interpreting the file as an SSH-1 public key. + */ + char *line, *p, *bitsp, *expp, *modp, *commentp; + + rewind(fp); + line = chomp(fgetline(fp)); + p = line; + + bitsp = p; + p += strspn(p, "0123456789"); + if (*p != ' ') + goto not_public_either; + *p++ = '\0'; + + expp = p; + p += strspn(p, "0123456789"); + if (*p != ' ') + goto not_public_either; + *p++ = '\0'; + + modp = p; + p += strspn(p, "0123456789"); + if (*p) { + if (*p != ' ') + goto not_public_either; + *p++ = '\0'; + commentp = p; + } else { + commentp = NULL; + } + + memset(&key, 0, sizeof(key)); + key.exponent = bignum_from_decimal(expp); + key.modulus = bignum_from_decimal(modp); + if (atoi(bitsp) != bignum_bitcount(key.modulus)) { + freebn(key.exponent); + freebn(key.modulus); + sfree(line); + error = "key bit count does not match in SSH-1 public key file"; + goto end; + } + if (commentptr) + *commentptr = commentp ? dupstr(commentp) : NULL; + *blob = rsa_public_blob(&key, bloblen); + freersakey(&key); + return 1; + + not_public_either: + sfree(line); error = "not an SSH-1 RSA file"; } @@ -840,6 +892,214 @@ struct ssh2_userkey *ssh2_load_userkey(const Filename *filename, return ret; } +unsigned char *rfc4716_loadpub(FILE *fp, char **algorithm, + int *pub_blob_len, char **commentptr, + const char **errorstr) +{ + const char *error; + char *line, *colon, *value; + char *comment = NULL; + unsigned char *pubblob = NULL; + int pubbloblen, pubblobsize; + char base64in[4]; + unsigned char base64out[3]; + int base64bytes; + int alglen; + + line = chomp(fgetline(fp)); + if (!line || 0 != strcmp(line, "---- BEGIN SSH2 PUBLIC KEY ----")) { + error = "invalid begin line in SSH-2 public key file"; + goto error; + } + sfree(line); line = NULL; + + while (1) { + line = chomp(fgetline(fp)); + if (!line) { + error = "truncated SSH-2 public key file"; + goto error; + } + colon = strstr(line, ": "); + if (!colon) + break; + *colon = '\0'; + value = colon + 2; + + if (!strcmp(line, "Comment")) { + char *p, *q; + + /* Remove containing double quotes, if present */ + p = value; + if (*p == '"' && p[strlen(p)-1] == '"') { + p[strlen(p)-1] = '\0'; + p++; + } + + /* Remove \-escaping, not in RFC4716 but seen in the wild + * in practice. */ + for (q = line; *p; p++) { + if (*p == '\\' && p[1]) + p++; + *q++ = *p; + } + + *q = '\0'; + comment = dupstr(line); + } else if (!strcmp(line, "Subject") || + !strncmp(line, "x-", 2)) { + /* Headers we recognise and ignore. Do nothing. */ + } else { + error = "unrecognised header in SSH-2 public key file"; + goto error; + } + + sfree(line); line = NULL; + } + + /* + * Now line contains the initial line of base64 data. Loop round + * while it still does contain base64. + */ + pubblobsize = 4096; + pubblob = snewn(pubblobsize, unsigned char); + pubbloblen = 0; + base64bytes = 0; + while (line && line[0] != '-') { + char *p; + for (p = line; *p; p++) { + base64in[base64bytes++] = *p; + if (base64bytes == 4) { + int n = base64_decode_atom(base64in, base64out); + if (pubbloblen + n > pubblobsize) { + pubblobsize = (pubbloblen + n) * 5 / 4 + 1024; + pubblob = sresize(pubblob, pubblobsize, unsigned char); + } + memcpy(pubblob + pubbloblen, base64out, n); + pubbloblen += n; + base64bytes = 0; + } + } + sfree(line); line = NULL; + line = chomp(fgetline(fp)); + } + + /* + * Finally, check the END line makes sense. + */ + if (!line || 0 != strcmp(line, "---- END SSH2 PUBLIC KEY ----")) { + error = "invalid end line in SSH-2 public key file"; + goto error; + } + sfree(line); line = NULL; + + /* + * OK, we now have a public blob and optionally a comment. We must + * return the key algorithm string too, so look for that at the + * start of the public blob. + */ + if (pubbloblen < 4) { + error = "not enough data in SSH-2 public key file"; + goto error; + } + alglen = toint(GET_32BIT(pubblob)); + if (alglen < 0 || alglen > pubbloblen-4) { + error = "invalid algorithm prefix in SSH-2 public key file"; + goto error; + } + if (algorithm) + *algorithm = dupprintf("%.*s", alglen, pubblob+4); + if (pub_blob_len) + *pub_blob_len = pubbloblen; + if (commentptr) + *commentptr = comment; + else + sfree(comment); + return pubblob; + + error: + sfree(line); + sfree(comment); + sfree(pubblob); + if (errorstr) + *errorstr = error; + return NULL; +} + +unsigned char *openssh_loadpub(FILE *fp, char **algorithm, + int *pub_blob_len, char **commentptr, + const char **errorstr) +{ + const char *error; + char *line, *base64; + char *comment = NULL; + unsigned char *pubblob = NULL; + int pubbloblen, pubblobsize; + int alglen; + + line = chomp(fgetline(fp)); + + base64 = strchr(line, ' '); + if (!base64) { + error = "no key blob in OpenSSH public key file"; + goto error; + } + *base64++ = '\0'; + + comment = strchr(base64, ' '); + if (comment) { + *comment++ = '\0'; + comment = dupstr(comment); + } + + pubblobsize = strlen(base64) / 4 * 3; + pubblob = snewn(pubblobsize, unsigned char); + pubbloblen = 0; + + while (!memchr(base64, '\0', 4)) { + assert(pubbloblen + 3 <= pubblobsize); + pubbloblen += base64_decode_atom(base64, pubblob + pubbloblen); + base64 += 4; + } + if (*base64) { + error = "invalid length for base64 data in OpenSSH public key file"; + goto error; + } + + /* + * Sanity check: the first word on the line should be the key + * algorithm, and should match the encoded string at the start of + * the public blob. + */ + alglen = strlen(line); + if (pubbloblen < alglen + 4 || + GET_32BIT(pubblob) != alglen || + 0 != memcmp(pubblob + 4, line, alglen)) { + error = "key algorithms do not match in OpenSSH public key file"; + goto error; + } + + /* + * Done. + */ + if (algorithm) + *algorithm = dupstr(line); + if (pub_blob_len) + *pub_blob_len = pubbloblen; + if (commentptr) + *commentptr = comment; + else + sfree(comment); + return pubblob; + + error: + sfree(line); + sfree(comment); + sfree(pubblob); + if (errorstr) + *errorstr = error; + return NULL; +} + unsigned char *ssh2_userkey_loadpub(const Filename *filename, char **algorithm, int *pub_blob_len, char **commentptr, const char **errorstr) @@ -849,7 +1109,7 @@ unsigned char *ssh2_userkey_loadpub(const Filename *filename, char **algorithm, const struct ssh_signkey *alg; unsigned char *public_blob; int public_blob_len; - int i; + int type, i; const char *error = NULL; char *comment = NULL; @@ -861,6 +1121,24 @@ unsigned char *ssh2_userkey_loadpub(const Filename *filename, char **algorithm, goto error; } + /* Initially, check if this is a public-only key file. Sometimes + * we'll be asked to read a public blob from one of those. */ + type = key_type_fp(fp); + if (type == SSH_KEYTYPE_SSH2_PUBLIC_RFC4716) { + unsigned char *ret = rfc4716_loadpub(fp, algorithm, pub_blob_len, + commentptr, errorstr); + fclose(fp); + return ret; + } else if (type == SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH) { + unsigned char *ret = openssh_loadpub(fp, algorithm, pub_blob_len, + commentptr, errorstr); + fclose(fp); + return ret; + } else if (type != SSH_KEYTYPE_SSH2) { + error = "not a PuTTY SSH-2 private key"; + goto error; + } + /* Read the first header line which contains the key type. */ if (!read_header(fp, header) || (0 != strcmp(header, "PuTTY-User-Key-File-2") && @@ -1156,30 +1434,32 @@ int ssh2_save_userkey(const Filename *filename, struct ssh2_userkey *key, } /* ---------------------------------------------------------------------- - * A function to determine the type of a private key file. Returns - * 0 on failure, 1 or 2 on success. + * Determine the type of a private key file. */ -int key_type(const Filename *filename) +static int key_type_fp(FILE *fp) { - FILE *fp; - char buf[32]; + char buf[1024]; + const char public_std_sig[] = "---- BEGIN SSH2 PUBLIC KEY"; const char putty2_sig[] = "PuTTY-User-Key-File-"; const char sshcom_sig[] = "---- BEGIN SSH2 ENCRYPTED PRIVAT"; const char openssh_new_sig[] = "-----BEGIN OPENSSH PRIVATE KEY"; const char openssh_sig[] = "-----BEGIN "; int i; + char *p; + + i = fread(buf, 1, sizeof(buf)-1, fp); + rewind(fp); - fp = f_open(filename, "r", FALSE); - if (!fp) - return SSH_KEYTYPE_UNOPENABLE; - i = fread(buf, 1, sizeof(buf), fp); - fclose(fp); if (i < 0) return SSH_KEYTYPE_UNOPENABLE; if (i < 32) return SSH_KEYTYPE_UNKNOWN; + assert(i > 0 && i < sizeof(buf)); + buf[i] = '\0'; if (!memcmp(buf, rsa_signature, sizeof(rsa_signature)-1)) return SSH_KEYTYPE_SSH1; + if (!memcmp(buf, public_std_sig, sizeof(public_std_sig)-1)) + return SSH_KEYTYPE_SSH2_PUBLIC_RFC4716; if (!memcmp(buf, putty2_sig, sizeof(putty2_sig)-1)) return SSH_KEYTYPE_SSH2; if (!memcmp(buf, openssh_new_sig, sizeof(openssh_new_sig)-1)) @@ -1188,9 +1468,31 @@ int key_type(const Filename *filename) return SSH_KEYTYPE_OPENSSH_PEM; if (!memcmp(buf, sshcom_sig, sizeof(sshcom_sig)-1)) return SSH_KEYTYPE_SSHCOM; + if ((p = buf + strspn(buf, "0123456789"), *p == ' ') && + (p = p+1 + strspn(p+1, "0123456789"), *p == ' ') && + (p = p+1 + strspn(p+1, "0123456789"), *p == ' ' || *p == '\n' || !*p)) + return SSH_KEYTYPE_SSH1_PUBLIC; + if ((p = buf + strcspn(buf, " "), find_pubkey_alg_len(p-buf, buf)) && + (p = p+1 + strspn(p+1, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij" + "klmnopqrstuvwxyz+/="), + *p == ' ' || *p == '\n' || !*p)) + return SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH; return SSH_KEYTYPE_UNKNOWN; /* unrecognised or EOF */ } +int key_type(const Filename *filename) +{ + FILE *fp; + int ret; + + fp = f_open(filename, "r", FALSE); + if (!fp) + return SSH_KEYTYPE_UNOPENABLE; + ret = key_type_fp(fp); + fclose(fp); + return ret; +} + /* * Convert the type word to a string, for `wrong type' error * messages. @@ -1199,7 +1501,10 @@ char *key_type_to_str(int type) { switch (type) { case SSH_KEYTYPE_UNOPENABLE: return "unable to open file"; break; - case SSH_KEYTYPE_UNKNOWN: return "not a private key"; break; + case SSH_KEYTYPE_UNKNOWN: return "not a recognised key file format"; break; + case SSH_KEYTYPE_SSH1_PUBLIC: return "SSH-1 public key"; break; + case SSH_KEYTYPE_SSH2_PUBLIC_RFC4716: return "SSH-2 public key (RFC 4716 format)"; break; + case SSH_KEYTYPE_SSH2_PUBLIC_OPENSSH: return "SSH-2 public key (OpenSSH format)"; break; case SSH_KEYTYPE_SSH1: return "SSH-1 private key"; break; case SSH_KEYTYPE_SSH2: return "PuTTY SSH-2 private key"; break; case SSH_KEYTYPE_OPENSSH_PEM: return "OpenSSH SSH-2 private key (old PEM format)"; break;