Add import of ssh.com private keys.
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Mon, 13 May 2002 16:32:42 +0000 (16:32 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Mon, 13 May 2002 16:32:42 +0000 (16:32 +0000)
git-svn-id: svn://svn.tartarus.org/sgt/putty@1672 cda61777-01e9-0310-a592-d414129be87e

import.c

index d118cb6..19deb8c 100644 (file)
--- a/import.c
+++ b/import.c
@@ -26,6 +26,9 @@
 int openssh_encrypted(char *filename);
 struct ssh2_userkey *openssh_read(char *filename, char *passphrase);
 
+int sshcom_encrypted(char *filename, char **comment);
+struct ssh2_userkey *sshcom_read(char *filename, char *passphrase);
+
 /*
  * Given a key type, determine whether we know how to import it.
  */
@@ -33,6 +36,8 @@ int import_possible(int type)
 {
     if (type == SSH_KEYTYPE_OPENSSH)
        return 1;
+    if (type == SSH_KEYTYPE_SSHCOM)
+       return 1;
     return 0;
 }
 
@@ -58,6 +63,9 @@ int import_encrypted(char *filename, int type, char **comment)
        *comment = filename;           /* OpenSSH doesn't do key comments */
        return openssh_encrypted(filename);
     }
+    if (type == SSH_KEYTYPE_SSHCOM) {
+       return sshcom_encrypted(filename, comment);
+    }
     return 0;
 }
 
@@ -76,6 +84,8 @@ struct ssh2_userkey *import_ssh2(char *filename, int type, char *passphrase)
 {
     if (type == SSH_KEYTYPE_OPENSSH)
        return openssh_read(filename, passphrase);
+    if (type == SSH_KEYTYPE_SSHCOM)
+       return sshcom_read(filename, passphrase);
     return NULL;
 }
 
@@ -157,6 +167,32 @@ int ber_read_id_len(void *source, int sourcelen,
     return p - (unsigned char *) source;
 }
 
+static int put_string(void *target, void *data, int len)
+{
+    unsigned char *d = (unsigned char *)target;
+
+    PUT_32BIT(d, len);
+    memcpy(d+4, data, len);
+    return len+4;
+}
+
+static int put_mp(void *target, void *data, int len)
+{
+    unsigned char *d = (unsigned char *)target;
+    unsigned char *i = (unsigned char *)data;
+
+    if (*i & 0x80) {
+        PUT_32BIT(d, len+1);
+        d[4] = 0;
+        memcpy(d+5, data, len);
+        return len+5;
+    } else {
+        PUT_32BIT(d, len);
+        memcpy(d+4, data, len);
+        return len+4;
+    }
+}
+
 /* ----------------------------------------------------------------------
  * Code to read OpenSSH private keys.
  */
@@ -493,3 +529,480 @@ struct ssh2_userkey *openssh_read(char *filename, char *passphrase)
     sfree(key);
     return retval;
 }
+
+/* ----------------------------------------------------------------------
+ * Code to read ssh.com private keys.
+ */
+
+/*
+ * The format of the base64 blob is largely ssh2-packet-formatted,
+ * except that mpints are a bit different: they're more like the
+ * old ssh1 mpint. You have a 32-bit bit count N, followed by
+ * (N+7)/8 bytes of data.
+ * 
+ * So. The blob contains:
+ * 
+ *  - uint32 0x3f6ff9eb       (magic number)
+ *  - uint32 size             (total blob size)
+ *  - string key-type         (see below)
+ *  - string cipher-type      (tells you if key is encrypted)
+ *  - string encrypted-blob
+ * 
+ * (The first size field includes the size field itself and the
+ * magic number before it. All other size fields are ordinary ssh2
+ * strings, so the size field indicates how much data is to
+ * _follow_.)
+ * 
+ * The encrypted blob, once decrypted, contains a single string
+ * which in turn contains the payload. (This allows padding to be
+ * added after that string while still making it clear where the
+ * real payload ends. Also it probably makes for a reasonable
+ * decryption check.)
+ * 
+ * The payload blob, for an RSA key, contains:
+ *  - mpint e
+ *  - mpint d
+ *  - mpint n  (yes, the public and private stuff is intermixed)
+ *  - mpint u  (presumably inverse of p mod q)
+ *  - mpint p  (p is the smaller prime)
+ *  - mpint q  (q is the larger)
+ * 
+ * For a DSA key, the payload blob contains:
+ *  - uint32 0
+ *  - mpint p
+ *  - mpint g
+ *  - mpint q
+ *  - mpint y
+ *  - mpint x
+ * 
+ * Alternatively, if the parameters are `predefined', that
+ * (0,p,g,q) sequence can be replaced by a uint32 1 and a string
+ * containing some predefined parameter specification. *shudder*,
+ * but I doubt we'll encounter this in real life.
+ * 
+ * The key type strings are ghastly. The RSA key I looked at had a
+ * type string of
+ * 
+ *   `if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}'
+ * 
+ * and the DSA key wasn't much better:
+ * 
+ *   `dl-modp{sign{dsa-nist-sha1},dh{plain}}'
+ * 
+ * It isn't clear that these will always be the same. I think it
+ * might be wise just to look at the `if-modn{sign{rsa' and
+ * `dl-modp{sign{dsa' prefixes.
+ * 
+ * Finally, the encryption. The cipher-type string appears to be
+ * either `none' or `3des-cbc'. Looks as if this is SSH2-style
+ * 3des-cbc (i.e. outer cbc rather than inner). The key is created
+ * from the passphrase by means of yet another hashing faff:
+ * 
+ *  - first 16 bytes are MD5(passphrase)
+ *  - next 16 bytes are MD5(passphrase || first 16 bytes)
+ *  - if there were more, they'd be MD5(passphrase || first 32),
+ *    and so on.
+ */
+
+struct sshcom_key {
+    char comment[256];                 /* allowing any length is overkill */
+    unsigned char *keyblob;
+    int keyblob_len, keyblob_size;
+};
+
+struct sshcom_key *load_sshcom_key(char *filename)
+{
+    struct sshcom_key *ret;
+    FILE *fp;
+    char buffer[256];
+    int len;
+    char *errmsg, *p;
+    int headers_done;
+    char base64_bit[4];
+    int base64_chars = 0;
+
+    ret = smalloc(sizeof(*ret));
+    ret->comment[0] = '\0';
+    ret->keyblob = NULL;
+    ret->keyblob_len = ret->keyblob_size = 0;
+
+    fp = fopen(filename, "r");
+    if (!fp) {
+       errmsg = "Unable to open key file";
+       goto error;
+    }
+    if (!fgets(buffer, sizeof(buffer), fp) ||
+       0 != strcmp(buffer, "---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----\n")) {
+       errmsg = "File does not begin with ssh.com key header";
+       goto error;
+    }
+
+    headers_done = 0;
+    while (1) {
+       if (!fgets(buffer, sizeof(buffer), fp)) {
+           errmsg = "Unexpected end of file";
+           goto error;
+       }
+        if (!strcmp(buffer, "---- END SSH2 ENCRYPTED PRIVATE KEY ----\n"))
+            break;                     /* done */
+       if ((p = strchr(buffer, ':')) != NULL) {
+           if (headers_done) {
+               errmsg = "Header found in body of key data";
+               goto error;
+           }
+           *p++ = '\0';
+           while (*p && isspace((unsigned char)*p)) p++;
+            /*
+             * Header lines can end in a trailing backslash for
+             * continuation.
+             */
+            while ((len = strlen(p)) > sizeof(buffer) - (p-buffer) -1 ||
+                   p[len-1] != '\n' || p[len-2] == '\\') {
+                if (len > (p-buffer) + sizeof(buffer)-2) {
+                    errmsg = "Header line too long to deal with";
+                    goto error;
+                }
+                if (!fgets(p+len-2, sizeof(buffer)-(p-buffer)-(len-2), fp)) {
+                    errmsg = "Unexpected end of file";
+                    goto error;
+                }
+            }
+            p[strcspn(p, "\n")] = '\0';
+            if (!strcmp(buffer, "Comment")) {
+                /* Strip quotes in comment if present. */
+                if (p[0] == '"' && p[strlen(p)-1] == '"') {
+                    p++;
+                    p[strlen(p)-1] = '\0';
+                }
+                strncpy(ret->comment, p, sizeof(ret->comment));
+                ret->comment[sizeof(ret->comment)-1] = '\0';
+            }
+       } else {
+           headers_done = 1;
+
+           p = buffer;
+           while (isbase64(*p)) {
+                base64_bit[base64_chars++] = *p;
+                if (base64_chars == 4) {
+                    unsigned char out[3];
+
+                    base64_chars = 0;
+
+                    len = base64_decode_atom(base64_bit, out);
+
+                    if (len <= 0) {
+                        errmsg = "Invalid base64 encoding";
+                        goto error;
+                    }
+
+                    if (ret->keyblob_len + len > ret->keyblob_size) {
+                        ret->keyblob_size = ret->keyblob_len + len + 256;
+                        ret->keyblob = srealloc(ret->keyblob, ret->keyblob_size);
+                    }
+
+                    memcpy(ret->keyblob + ret->keyblob_len, out, len);
+                    ret->keyblob_len += len;
+                }
+
+               p++;
+           }
+       }
+    }
+
+    if (ret->keyblob_len == 0 || !ret->keyblob) {
+       errmsg = "Key body not present";
+       goto error;
+    }
+
+    return ret;
+
+    error:
+    if (ret) {
+       if (ret->keyblob) sfree(ret->keyblob);
+       sfree(ret);
+    }
+    return NULL;
+}
+
+int sshcom_encrypted(char *filename, char **comment)
+{
+    struct sshcom_key *key = load_sshcom_key(filename);
+    int pos, len, answer;
+
+    *comment = NULL;
+    if (!key)
+        return 0;
+
+    /*
+     * Check magic number.
+     */
+    if (GET_32BIT(key->keyblob) != 0x3f6ff9eb)
+        return 0;                      /* key is invalid */
+
+    /*
+     * Find the cipher-type string.
+     */
+    answer = 0;
+    pos = 8;
+    if (key->keyblob_len < pos+4)
+        goto done;                     /* key is far too short */
+    pos += 4 + GET_32BIT(key->keyblob + pos);   /* skip key type */
+    if (key->keyblob_len < pos+4)
+        goto done;                     /* key is far too short */
+    len = GET_32BIT(key->keyblob + pos);   /* find cipher-type length */
+    if (key->keyblob_len < pos+4+len)
+        goto done;                     /* cipher type string is incomplete */
+    if (len != 4 || 0 != memcmp(key->keyblob + pos + 4, "none", 4))
+        answer = 1;
+
+    done:
+    *comment = dupstr(key->comment);
+    sfree(key->keyblob);
+    sfree(key);
+    return answer;
+}
+
+struct mpint_pos { void *start; int bytes; };
+
+int sshcom_read_mpint(void *data, int len, struct mpint_pos *ret)
+{
+    int bits;
+    int bytes;
+    unsigned char *d = (unsigned char *) data;
+
+    if (len < 4)
+        goto error;
+    bits = GET_32BIT(d);
+
+    bytes = (bits + 7) / 8;
+    if (len < 4+bytes)
+        goto error;
+
+    ret->start = d + 4;
+    ret->bytes = bytes;
+    return bytes+4;
+
+    error:
+    ret->start = NULL;
+    ret->bytes = -1;
+    return len;                        /* ensure further calls fail as well */
+}
+
+struct ssh2_userkey *sshcom_read(char *filename, char *passphrase)
+{
+    struct sshcom_key *key = load_sshcom_key(filename);
+    char *errmsg;
+    int pos, len;
+    const char prefix_rsa[] = "if-modn{sign{rsa";
+    const char prefix_dsa[] = "dl-modp{sign{dsa";
+    enum { RSA, DSA } type;
+    int encrypted;
+    char *ciphertext;
+    int cipherlen;
+    struct ssh2_userkey *ret = NULL, *retkey;
+    const struct ssh_signkey *alg;
+    unsigned char *blob = NULL;
+    int publen, privlen;
+
+    if (!key)
+        return NULL;
+
+    /*
+     * Check magic number.
+     */
+    if (GET_32BIT(key->keyblob) != 0x3f6ff9eb) {
+        errmsg = "Key does not begin with magic number";
+        goto error;
+    }
+
+    /*
+     * Determine the key type.
+     */
+    pos = 8;
+    if (key->keyblob_len < pos+4 ||
+        (len = GET_32BIT(key->keyblob + pos)) > key->keyblob_len - pos - 4) {
+        errmsg = "Key blob does not contain a key type string";
+        goto error;
+    }
+    if (len > sizeof(prefix_rsa) - 1 &&
+        !memcmp(key->keyblob+pos+4, prefix_rsa, sizeof(prefix_rsa) - 1)) {
+        type = RSA;
+    } else if (len > sizeof(prefix_dsa) - 1 &&
+        !memcmp(key->keyblob+pos+4, prefix_dsa, sizeof(prefix_dsa) - 1)) {
+        type = DSA;
+    } else {
+        errmsg = "Key is of unknown type";
+        goto error;
+    }
+    pos += 4+len;
+
+    /*
+     * Determine the cipher type.
+     */
+    if (key->keyblob_len < pos+4 ||
+        (len = GET_32BIT(key->keyblob + pos)) > key->keyblob_len - pos - 4) {
+        errmsg = "Key blob does not contain a cipher type string";
+        goto error;
+    }
+    if (len == 4 && !memcmp(key->keyblob+pos+4, "none", 4))
+        encrypted = 0;
+    else if (len == 8 && !memcmp(key->keyblob+pos+4, "3des-cbc", 8))
+        encrypted = 1;
+    else {
+        errmsg = "Key encryption is of unknown type";
+        goto error;
+    }
+    pos += 4+len;
+
+    /*
+     * Get hold of the encrypted part of the key.
+     */
+    if (key->keyblob_len < pos+4 ||
+        (len = GET_32BIT(key->keyblob + pos)) > key->keyblob_len - pos - 4) {
+        errmsg = "Key blob does not contain actual key data";
+        goto error;
+    }
+    ciphertext = key->keyblob + pos + 4;
+    cipherlen = len;
+    if (cipherlen == 0) {
+        errmsg = "Length of key data is zero";
+        goto error;
+    }
+
+    /*
+     * Decrypt it if necessary.
+     */
+    if (encrypted) {
+       /*
+        * Derive encryption key from passphrase and iv/salt:
+        * 
+        *  - let block A equal MD5(passphrase)
+        *  - let block B equal MD5(passphrase || A)
+        *  - block C would be MD5(passphrase || A || B) and so on
+        *  - encryption key is the first N bytes of A || B
+        */
+       struct MD5Context md5c;
+       unsigned char keybuf[32], iv[8];
+
+        if (cipherlen % 8 != 0) {
+            errmsg = "Encrypted part of key is not a multiple of cipher block"
+                " size";
+            goto error;
+        }
+
+       MD5Init(&md5c);
+       MD5Update(&md5c, passphrase, strlen(passphrase));
+       MD5Final(keybuf, &md5c);
+
+       MD5Init(&md5c);
+       MD5Update(&md5c, passphrase, strlen(passphrase));
+       MD5Update(&md5c, keybuf, 16);
+       MD5Final(keybuf+16, &md5c);
+
+       /*
+        * Now decrypt the key blob.
+        */
+        memset(iv, 0, 8);
+       des3_decrypt_pubkey_ossh(keybuf, iv, ciphertext, cipherlen);
+
+        /*
+         * Hereafter we return WRONG_PASSPHRASE for any parsing
+         * error. (But not if we haven't just tried to decrypt it!)
+         */
+        if (encrypted)
+            ret = SSH2_WRONG_PASSPHRASE;
+    }
+
+    /*
+     * Strip away the containing string to get to the real meat.
+     */
+    len = GET_32BIT(ciphertext);
+    if (len > cipherlen-4) {
+        errmsg = "containing string was ill-formed";
+        goto error;
+    }
+    ciphertext += 4;
+    cipherlen = len;
+
+    /*
+     * Now we break down into RSA versus DSA. In either case we'll
+     * construct public and private blobs in our own format, and
+     * end up feeding them to alg->createkey().
+     */
+    blob = smalloc(cipherlen + 256);
+    privlen = 0;
+    if (type == RSA) {
+        struct mpint_pos n, e, d, u, p, q;
+        int pos = 0;
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &e);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &d);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &n);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &u);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &p);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &q);
+        if (!q.start) {
+            errmsg = "key data did not contain six integers";
+            goto error;
+        }
+
+        alg = &ssh_rsa;
+        pos = 0;
+        pos += put_string(blob+pos, "ssh-rsa", 7);
+        pos += put_mp(blob+pos, e.start, e.bytes);
+        pos += put_mp(blob+pos, n.start, n.bytes);
+        publen = pos;
+        pos += put_string(blob+pos, d.start, d.bytes);
+        pos += put_mp(blob+pos, q.start, q.bytes);
+        pos += put_mp(blob+pos, p.start, p.bytes);
+        pos += put_mp(blob+pos, u.start, u.bytes);
+        privlen = pos - publen;
+    } else if (type == DSA) {
+        struct mpint_pos p, q, g, x, y;
+        int pos = 4;
+        if (GET_32BIT(ciphertext) != 0) {
+            errmsg = "predefined DSA parameters not supported";
+            goto error;
+        }
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &p);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &g);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &q);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &y);
+        pos += sshcom_read_mpint(ciphertext+pos, cipherlen-pos, &x);
+        if (!x.start) {
+            errmsg = "key data did not contain five integers";
+            goto error;
+        }
+
+        alg = &ssh_dss;
+        pos = 0;
+        pos += put_string(blob+pos, "ssh-dss", 7);
+        pos += put_mp(blob+pos, p.start, p.bytes);
+        pos += put_mp(blob+pos, q.start, q.bytes);
+        pos += put_mp(blob+pos, g.start, g.bytes);
+        pos += put_mp(blob+pos, y.start, y.bytes);
+        publen = pos;
+        pos += put_mp(blob+pos, x.start, x.bytes);
+        privlen = pos - publen;
+    }
+
+    assert(privlen > 0);              /* should have bombed by now if not */
+
+    retkey = smalloc(sizeof(struct ssh2_userkey));
+    retkey->alg = alg;
+    retkey->data = alg->createkey(blob, publen, blob+publen, privlen);
+    if (!retkey->data) {
+       sfree(retkey);
+       errmsg = "unable to create key data structure";
+       goto error;
+    }
+    retkey->comment = dupstr(key->comment);
+
+    errmsg = NULL; /* no error */
+    ret = retkey;
+
+    error:
+    if (blob) sfree(blob);
+    sfree(key->keyblob);
+    sfree(key);
+    return ret;
+}