Pageant is now able to avoid asking for the passphrase when asked to
[u/mdw/putty] / sshpubk.c
index d5576e8..aa65d98 100644 (file)
--- a/sshpubk.c
+++ b/sshpubk.c
@@ -10,6 +10,7 @@
 #include <assert.h>
 
 #include "ssh.h"
+#include "misc.h"
 
 #define PUT_32BIT(cp, value) do { \
   (cp)[3] = (value); \
@@ -31,7 +32,7 @@
                           (x)=='+' ? 62 : \
                           (x)=='/' ? 63 : 0 )
 
-static int loadrsakey_main(FILE * fp, struct RSAKey *key,
+static int loadrsakey_main(FILE * fp, struct RSAKey *key, int pub_only,
                           char **commentptr, char *passphrase)
 {
     unsigned char buf[16384];
@@ -75,6 +76,11 @@ static int loadrsakey_main(FILE * fp, struct RSAKey *key,
     if (len - i < 0)
        goto end;                      /* overran */
 
+    if (pub_only) {
+       ret = 1;
+       goto end;
+    }
+
     /* Next, the comment field. */
     j = GET_32BIT(buf + i);
     i += 4;
@@ -160,7 +166,7 @@ int loadrsakey(char *filename, struct RSAKey *key, char *passphrase)
      * key file.
      */
     if (fgets(buf, sizeof(buf), fp) && !strcmp(buf, rsa_signature)) {
-       return loadrsakey_main(fp, key, NULL, passphrase);
+       return loadrsakey_main(fp, key, FALSE, NULL, passphrase);
     }
 
     /*
@@ -188,13 +194,50 @@ int rsakey_encrypted(char *filename, char **comment)
      * key file.
      */
     if (fgets(buf, sizeof(buf), fp) && !strcmp(buf, rsa_signature)) {
-       return loadrsakey_main(fp, NULL, comment, NULL);
+       return loadrsakey_main(fp, NULL, FALSE, comment, NULL);
     }
     fclose(fp);
     return 0;                         /* wasn't the right kind of file */
 }
 
 /*
+ * Return a malloc'ed chunk of memory containing the public blob of
+ * an RSA key, as given in the agent protocol (modulus bits,
+ * exponent, modulus).
+ */
+int rsakey_pubblob(char *filename, void **blob, int *bloblen)
+{
+    FILE *fp;
+    unsigned char buf[64];
+    struct RSAKey key;
+    int ret;
+
+    /* Default return if we fail. */
+    *blob = NULL;
+    *bloblen = 0;
+    ret = 0;
+
+    fp = fopen(filename, "rb");
+    if (!fp)
+       return 0;                      /* doesn't even exist */
+
+    /*
+     * Read the first line of the file and see if it's a v1 private
+     * key file.
+     */
+    if (fgets(buf, sizeof(buf), fp) && !strcmp(buf, rsa_signature)) {
+       memset(&key, 0, sizeof(key));
+       if (loadrsakey_main(fp, &key, TRUE, NULL, NULL)) {
+           *blob = rsa_public_blob(&key, bloblen);
+           freersakey(&key);
+           ret = 1;
+       }
+    }
+    fclose(fp);
+    return ret;
+}
+
+/*
  * Save an RSA key file. Return nonzero on success.
  */
 int saversakey(char *filename, struct RSAKey *key, char *passphrase)
@@ -302,64 +345,83 @@ int saversakey(char *filename, struct RSAKey *key, char *passphrase)
 
 /*
  * PuTTY's own format for SSH2 keys is as follows:
- * 
+ *
  * The file is text. Lines are terminated by CRLF, although CR-only
  * and LF-only are tolerated on input.
- * 
- * The first line says "PuTTY-User-Key-File-1: " plus the name of the
- * algorithm ("ssh-dss", "ssh-rsa" etc. Although, of course, this
- * being PuTTY, "ssh-dss" is not supported.)
- * 
+ *
+ * The first line says "PuTTY-User-Key-File-2: " plus the name of the
+ * algorithm ("ssh-dss", "ssh-rsa" etc).
+ *
  * The next line says "Encryption: " plus an encryption type.
  * Currently the only supported encryption types are "aes256-cbc"
  * and "none".
- * 
+ *
  * The next line says "Comment: " plus the comment string.
- * 
+ *
  * Next there is a line saying "Public-Lines: " plus a number N.
  * The following N lines contain a base64 encoding of the public
  * part of the key. This is encoded as the standard SSH2 public key
  * blob (with no initial length): so for RSA, for example, it will
  * read
- * 
+ *
  *    string "ssh-rsa"
  *    mpint  exponent
  *    mpint  modulus
- * 
+ *
  * Next, there is a line saying "Private-Lines: " plus a number N,
  * and then N lines containing the (potentially encrypted) private
  * part of the key. For the key type "ssh-rsa", this will be
  * composed of
- * 
+ *
  *    mpint  private_exponent
  *    mpint  p                  (the larger of the two primes)
  *    mpint  q                  (the smaller prime)
  *    mpint  iqmp               (the inverse of q modulo p)
  *    data   padding            (to reach a multiple of the cipher block size)
+ *
+ * And for "ssh-dss", it will be composed of
+ *
+ *    mpint  x                  (the private key parameter)
+ *  [ string hash   20-byte hash of mpints p || q || g   only in old format ]
+ * 
+ * Finally, there is a line saying "Private-MAC: " plus a hex
+ * representation of a HMAC-SHA-1 of:
+ *
+ *    string  name of algorithm ("ssh-dss", "ssh-rsa")
+ *    string  encryption type
+ *    string  comment
+ *    string  public-blob
+ *    string  private-plaintext (the plaintext version of the
+ *                               private part, including the final
+ *                               padding)
  * 
- * Finally, there is a line saying "Private-Hash: " plus a hex
- * representation of a SHA-1 hash of the plaintext version of the
- * private part, including the final padding.
+ * The key to the MAC is itself a SHA-1 hash of:
  * 
+ *    data    "putty-private-key-file-mac-key"
+ *    data    passphrase
+ *
+ * Encrypted keys should have a MAC, whereas unencrypted ones must
+ * have a hash.
+ *
  * If the key is encrypted, the encryption key is derived from the
  * passphrase by means of a succession of SHA-1 hashes. Each hash
  * is the hash of:
- * 
+ *
  *    uint32  sequence-number
- *    string  passphrase
- * 
+ *    data    passphrase
+ *
  * where the sequence-number increases from zero. As many of these
  * hashes are used as necessary.
- * 
- * NOTE! It is important that all _public_ data can be verified
- * with reference to the _private_ data. There exist attacks based
- * on modifying the public key but leaving the private section
- * intact.
- * 
- * With RSA, this is easy: verify that n = p*q, and also verify
- * that e*d == 1 modulo (p-1)(q-1). With DSA (if we were ever to
- * support it), we would need to store extra data in the private
- * section other than just x.
+ *
+ * For backwards compatibility with snapshots between 0.51 and
+ * 0.52, we also support the older key file format, which begins
+ * with "PuTTY-User-Key-File-1" (version number differs). In this
+ * format the Private-MAC: field only covers the private-plaintext
+ * field and nothing else (and without the 4-byte string length on
+ * the front too). Moreover, for RSA keys the Private-MAC: field
+ * can be replaced with a Private-Hash: field which is a plain
+ * SHA-1 hash instead of an HMAC. This is not allowable in DSA
+ * keys. (Yes, the old format was a mess. Guess why it changed :-)
  */
 
 static int read_header(FILE * fp, char *header)
@@ -514,16 +576,17 @@ struct ssh2_userkey ssh2_wrong_passphrase = {
 struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
 {
     FILE *fp;
-    char header[40], *b, *comment, *hash;
+    char header[40], *b, *encryption, *comment, *mac;
     const struct ssh_signkey *alg;
     struct ssh2_userkey *ret;
     int cipher, cipherblk;
     unsigned char *public_blob, *private_blob;
     int public_blob_len, private_blob_len;
-    int i;
+    int i, is_mac, old_fmt;
+    int passlen = passphrase ? strlen(passphrase) : 0;
 
     ret = NULL;                               /* return NULL for most errors */
-    comment = hash = NULL;
+    comment = mac = NULL;
     public_blob = private_blob = NULL;
 
     fp = fopen(filename, "rb");
@@ -531,14 +594,23 @@ struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
        goto error;
 
     /* Read the first header line which contains the key type. */
-    if (!read_header(fp, header)
-       || 0 != strcmp(header, "PuTTY-User-Key-File-1"))
+    if (!read_header(fp, header))
+       goto error;
+    if (0 == strcmp(header, "PuTTY-User-Key-File-2")) {
+       old_fmt = 0;
+    } else if (0 == strcmp(header, "PuTTY-User-Key-File-1")) {
+       /* this is an old key file; warn and then continue */
+       old_keyfile_warning();
+       old_fmt = 1;
+    } else
        goto error;
     if ((b = read_body(fp)) == NULL)
        goto error;
-    /* Select key algorithm structure. Currently only ssh-rsa. */
+    /* Select key algorithm structure. */
     if (!strcmp(b, "ssh-rsa"))
        alg = &ssh_rsa;
+    else if (!strcmp(b, "ssh-dss"))
+       alg = &ssh_dss;
     else {
        sfree(b);
        goto error;
@@ -548,19 +620,18 @@ struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
     /* Read the Encryption header line. */
     if (!read_header(fp, header) || 0 != strcmp(header, "Encryption"))
        goto error;
-    if ((b = read_body(fp)) == NULL)
+    if ((encryption = read_body(fp)) == NULL)
        goto error;
-    if (!strcmp(b, "aes256-cbc")) {
+    if (!strcmp(encryption, "aes256-cbc")) {
        cipher = 1;
        cipherblk = 16;
-    } else if (!strcmp(b, "none")) {
+    } else if (!strcmp(encryption, "none")) {
        cipher = 0;
        cipherblk = 1;
     } else {
-       sfree(b);
+       sfree(encryption);
        goto error;
     }
-    sfree(b);
 
     /* Read the Comment header line. */
     if (!read_header(fp, header) || 0 != strcmp(header, "Comment"))
@@ -588,10 +659,19 @@ struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
     if ((private_blob = read_blob(fp, i, &private_blob_len)) == NULL)
        goto error;
 
-    /* Read the Private-Hash header line. */
-    if (!read_header(fp, header) || 0 != strcmp(header, "Private-Hash"))
+    /* Read the Private-MAC or Private-Hash header line. */
+    if (!read_header(fp, header))
        goto error;
-    if ((hash = read_body(fp)) == NULL)
+    if (0 == strcmp(header, "Private-MAC")) {
+       if ((mac = read_body(fp)) == NULL)
+           goto error;
+       is_mac = 1;
+    } else if (0 == strcmp(header, "Private-Hash") &&
+                          alg == &ssh_rsa && old_fmt) {
+       if ((mac = read_body(fp)) == NULL)
+           goto error;
+       is_mac = 0;
+    } else
        goto error;
 
     fclose(fp);
@@ -603,15 +683,12 @@ struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
     if (cipher) {
        unsigned char key[40];
        SHA_State s;
-       int passlen;
 
        if (!passphrase)
            goto error;
        if (private_blob_len % cipherblk)
            goto error;
 
-       passlen = strlen(passphrase);
-
        SHA_Init(&s);
        SHA_Bytes(&s, "\0\0\0\0", 4);
        SHA_Bytes(&s, passphrase, passlen);
@@ -624,24 +701,77 @@ struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
     }
 
     /*
-     * Verify the private hash.
+     * Verify the MAC.
      */
     {
-       char realhash[41];
+       char realmac[41];
        unsigned char binary[20];
+       unsigned char *macdata;
+       int maclen;
+       int free_macdata;
+
+       if (old_fmt) {
+           /* MAC (or hash) only covers the private blob. */
+           macdata = private_blob;
+           maclen = private_blob_len;
+           free_macdata = 0;
+       } else {
+           unsigned char *p;
+           int namelen = strlen(alg->name);
+           int enclen = strlen(encryption);
+           int commlen = strlen(comment);
+           maclen = (4 + namelen +
+                     4 + enclen +
+                     4 + commlen +
+                     4 + public_blob_len +
+                     4 + private_blob_len);
+           macdata = smalloc(maclen);
+           p = macdata;
+#define DO_STR(s,len) PUT_32BIT(p,(len));memcpy(p+4,(s),(len));p+=4+(len)
+           DO_STR(alg->name, namelen);
+           DO_STR(encryption, enclen);
+           DO_STR(comment, commlen);
+           DO_STR(public_blob, public_blob_len);
+           DO_STR(private_blob, private_blob_len);
+
+           free_macdata = 1;
+       }
+
+       if (is_mac) {
+           SHA_State s;
+           unsigned char mackey[20];
+           char header[] = "putty-private-key-file-mac-key";
+
+           SHA_Init(&s);
+           SHA_Bytes(&s, header, sizeof(header)-1);
+           if (passphrase)
+               SHA_Bytes(&s, passphrase, passlen);
+           SHA_Final(&s, mackey);
+
+           hmac_sha1_simple(mackey, 20, macdata, maclen, binary);
+
+           memset(mackey, 0, sizeof(mackey));
+           memset(&s, 0, sizeof(s));
+       } else {
+           SHA_Simple(macdata, maclen, binary);
+       }
+
+       if (free_macdata) {
+           memset(macdata, 0, maclen);
+           sfree(macdata);
+       }
 
-       SHA_Simple(private_blob, private_blob_len, binary);
        for (i = 0; i < 20; i++)
-           sprintf(realhash + 2 * i, "%02x", binary[i]);
+           sprintf(realmac + 2 * i, "%02x", binary[i]);
 
-       if (strcmp(hash, realhash)) {
-           /* An incorrect hash is an unconditional Error if the key is
+       if (strcmp(mac, realmac)) {
+           /* An incorrect MAC is an unconditional Error if the key is
             * unencrypted. Otherwise, it means Wrong Passphrase. */
            ret = cipher ? SSH2_WRONG_PASSPHRASE : NULL;
            goto error;
        }
     }
-    sfree(hash);
+    sfree(mac);
 
     /*
      * Create and return the key.
@@ -658,6 +788,7 @@ struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
     }
     sfree(public_blob);
     sfree(private_blob);
+    sfree(encryption);
     return ret;
 
     /*
@@ -668,8 +799,10 @@ struct ssh2_userkey *ssh2_load_userkey(char *filename, char *passphrase)
        fclose(fp);
     if (comment)
        sfree(comment);
-    if (hash)
-       sfree(hash);
+    if (encryption)
+       sfree(encryption);
+    if (mac)
+       sfree(mac);
     if (public_blob)
        sfree(public_blob);
     if (private_blob)
@@ -695,13 +828,16 @@ char *ssh2_userkey_loadpub(char *filename, char **algorithm,
 
     /* Read the first header line which contains the key type. */
     if (!read_header(fp, header)
-       || 0 != strcmp(header, "PuTTY-User-Key-File-1"))
+       || (0 != strcmp(header, "PuTTY-User-Key-File-2") &&
+           0 != strcmp(header, "PuTTY-User-Key-File-1")))
        goto error;
     if ((b = read_body(fp)) == NULL)
        goto error;
     /* Select key algorithm structure. Currently only ssh-rsa. */
     if (!strcmp(b, "ssh-rsa"))
        alg = &ssh_rsa;
+    else if (!strcmp(b, "ssh-dss"))
+       alg = &ssh_dss;
     else {
        sfree(b);
        goto error;
@@ -733,8 +869,10 @@ char *ssh2_userkey_loadpub(char *filename, char **algorithm,
        goto error;
 
     fclose(fp);
-    *pub_blob_len = public_blob_len;
-    *algorithm = alg->name;
+    if (pub_blob_len)
+       *pub_blob_len = public_blob_len;
+    if (algorithm)
+       *algorithm = alg->name;
     return public_blob;
 
     /*
@@ -761,7 +899,8 @@ int ssh2_userkey_encrypted(char *filename, char **commentptr)
     if (!fp)
        return 0;
     if (!read_header(fp, header)
-       || 0 != strcmp(header, "PuTTY-User-Key-File-1")) {
+       || (0 != strcmp(header, "PuTTY-User-Key-File-2") &&
+           0 != strcmp(header, "PuTTY-User-Key-File-1"))) {
        fclose(fp);
        return 0;
     }
@@ -865,7 +1004,7 @@ int ssh2_save_userkey(char *filename, struct ssh2_userkey *key,
     int cipherblk;
     int i;
     char *cipherstr;
-    unsigned char priv_hash[20];
+    unsigned char priv_mac[20];
 
     /*
      * Fetch the key component blobs.
@@ -895,13 +1034,48 @@ int ssh2_save_userkey(char *filename, struct ssh2_userkey *key,
     memcpy(priv_blob_encrypted, priv_blob, priv_blob_len);
     /* Create padding based on the SHA hash of the unpadded blob. This prevents
      * too easy a known-plaintext attack on the last block. */
-    SHA_Simple(priv_blob, priv_blob_len, priv_hash);
+    SHA_Simple(priv_blob, priv_blob_len, priv_mac);
     assert(priv_encrypted_len - priv_blob_len < 20);
-    memcpy(priv_blob_encrypted + priv_blob_len, priv_hash,
+    memcpy(priv_blob_encrypted + priv_blob_len, priv_mac,
           priv_encrypted_len - priv_blob_len);
 
-    /* Now create the _real_ private hash. */
-    SHA_Simple(priv_blob_encrypted, priv_encrypted_len, priv_hash);
+    /* Now create the MAC. */
+    {
+       unsigned char *macdata;
+       int maclen;
+       unsigned char *p;
+       int namelen = strlen(key->alg->name);
+       int enclen = strlen(cipherstr);
+       int commlen = strlen(key->comment);
+       SHA_State s;
+       unsigned char mackey[20];
+       char header[] = "putty-private-key-file-mac-key";
+
+       maclen = (4 + namelen +
+                 4 + enclen +
+                 4 + commlen +
+                 4 + pub_blob_len +
+                 4 + priv_encrypted_len);
+       macdata = smalloc(maclen);
+       p = macdata;
+#define DO_STR(s,len) PUT_32BIT(p,(len));memcpy(p+4,(s),(len));p+=4+(len)
+       DO_STR(key->alg->name, namelen);
+       DO_STR(cipherstr, enclen);
+       DO_STR(key->comment, commlen);
+       DO_STR(pub_blob, pub_blob_len);
+       DO_STR(priv_blob_encrypted, priv_encrypted_len);
+
+       SHA_Init(&s);
+       SHA_Bytes(&s, header, sizeof(header)-1);
+       if (passphrase)
+           SHA_Bytes(&s, passphrase, strlen(passphrase));
+       SHA_Final(&s, mackey);
+       hmac_sha1_simple(mackey, 20, macdata, maclen, priv_mac);
+       memset(macdata, 0, maclen);
+       sfree(macdata);
+       memset(mackey, 0, sizeof(mackey));
+       memset(&s, 0, sizeof(s));
+    }
 
     if (passphrase) {
        char key[40];
@@ -919,23 +1093,31 @@ int ssh2_save_userkey(char *filename, struct ssh2_userkey *key,
        SHA_Final(&s, key + 20);
        aes256_encrypt_pubkey(key, priv_blob_encrypted,
                              priv_encrypted_len);
+
+       memset(key, 0, sizeof(key));
+       memset(&s, 0, sizeof(s));
     }
 
     fp = fopen(filename, "w");
     if (!fp)
        return 0;
-    fprintf(fp, "PuTTY-User-Key-File-1: %s\n", key->alg->name);
+    fprintf(fp, "PuTTY-User-Key-File-2: %s\n", key->alg->name);
     fprintf(fp, "Encryption: %s\n", cipherstr);
     fprintf(fp, "Comment: %s\n", key->comment);
     fprintf(fp, "Public-Lines: %d\n", base64_lines(pub_blob_len));
     base64_encode(fp, pub_blob, pub_blob_len);
     fprintf(fp, "Private-Lines: %d\n", base64_lines(priv_encrypted_len));
     base64_encode(fp, priv_blob_encrypted, priv_encrypted_len);
-    fprintf(fp, "Private-Hash: ");
+    fprintf(fp, "Private-MAC: ");
     for (i = 0; i < 20; i++)
-       fprintf(fp, "%02x", priv_hash[i]);
+       fprintf(fp, "%02x", priv_mac[i]);
     fprintf(fp, "\n");
     fclose(fp);
+
+    sfree(pub_blob);
+    memset(priv_blob, 0, priv_blob_len);
+    sfree(priv_blob);
+    sfree(priv_blob_encrypted);
     return 1;
 }