mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-08 08:58:00 +00:00
f454c84a23
I'm preparing to be able to ask about the other end of the connection too, so the first step is to give this data structure a neutral name that can refer to either. No functional change yet.
2692 lines
84 KiB
C
2692 lines
84 KiB
C
/*
|
|
* pageant.c: cross-platform code to implement Pageant.
|
|
*/
|
|
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <assert.h>
|
|
|
|
#include "putty.h"
|
|
#include "mpint.h"
|
|
#include "ssh.h"
|
|
#include "sshcr.h"
|
|
#include "pageant.h"
|
|
|
|
/*
|
|
* We need this to link with the RSA code, because rsa_ssh1_encrypt()
|
|
* pads its data with random bytes. Since we only use rsa_ssh1_decrypt()
|
|
* and the signing functions, which are deterministic, this should
|
|
* never be called.
|
|
*
|
|
* If it _is_ called, there is a _serious_ problem, because it
|
|
* won't generate true random numbers. So we must scream, panic,
|
|
* and exit immediately if that should happen.
|
|
*/
|
|
void random_read(void *buf, size_t size)
|
|
{
|
|
modalfatalbox("Internal error: attempt to use random numbers in Pageant");
|
|
}
|
|
|
|
static bool pageant_local = false;
|
|
|
|
struct PageantClientDialogId {
|
|
int dummy;
|
|
};
|
|
|
|
typedef struct PageantPrivateKeySort PageantPrivateKeySort;
|
|
typedef struct PageantPublicKeySort PageantPublicKeySort;
|
|
typedef struct PageantPrivateKey PageantPrivateKey;
|
|
typedef struct PageantPublicKey PageantPublicKey;
|
|
typedef struct PageantAsyncOp PageantAsyncOp;
|
|
typedef struct PageantAsyncOpVtable PageantAsyncOpVtable;
|
|
typedef struct PageantClientRequestNode PageantClientRequestNode;
|
|
typedef struct PageantKeyRequestNode PageantKeyRequestNode;
|
|
|
|
struct PageantClientRequestNode {
|
|
PageantClientRequestNode *prev, *next;
|
|
};
|
|
struct PageantKeyRequestNode {
|
|
PageantKeyRequestNode *prev, *next;
|
|
};
|
|
|
|
struct PageantClientInfo {
|
|
PageantClient *pc; /* goes to NULL when client is unregistered */
|
|
PageantClientRequestNode head;
|
|
};
|
|
|
|
struct PageantAsyncOp {
|
|
const PageantAsyncOpVtable *vt;
|
|
PageantClientInfo *info;
|
|
PageantClientRequestNode cr;
|
|
PageantClientRequestId *reqid;
|
|
};
|
|
struct PageantAsyncOpVtable {
|
|
void (*coroutine)(PageantAsyncOp *pao);
|
|
void (*free)(PageantAsyncOp *pao);
|
|
};
|
|
static inline void pageant_async_op_coroutine(PageantAsyncOp *pao)
|
|
{ pao->vt->coroutine(pao); }
|
|
static inline void pageant_async_op_free(PageantAsyncOp *pao)
|
|
{
|
|
delete_callbacks_for_context(pao);
|
|
pao->vt->free(pao);
|
|
}
|
|
static inline void pageant_async_op_unlink(PageantAsyncOp *pao)
|
|
{
|
|
pao->cr.prev->next = pao->cr.next;
|
|
pao->cr.next->prev = pao->cr.prev;
|
|
}
|
|
static inline void pageant_async_op_unlink_and_free(PageantAsyncOp *pao)
|
|
{
|
|
pageant_async_op_unlink(pao);
|
|
pageant_async_op_free(pao);
|
|
}
|
|
static void pageant_async_op_callback(void *vctx)
|
|
{
|
|
pageant_async_op_coroutine((PageantAsyncOp *)vctx);
|
|
}
|
|
|
|
/*
|
|
* Master lists of all the keys we have stored, in any form at all.
|
|
*
|
|
* We store private and public keys in separate lists, because
|
|
* multiple public keys can share the same private key (due to one
|
|
* having a certificate and the other not, or having more than one
|
|
* different certificate). And when we decrypt or re-encrypt a private
|
|
* key, we don't really want to faff about doing it multiple times if
|
|
* there's more than one public key it goes with. If someone tries to
|
|
* re-encrypt a key to make their machine safer against unattended
|
|
* access, then it would be embarrassing to find they'd forgotten to
|
|
* re-encrypt the _other_ copy of it; conversely, once you've
|
|
* decrypted a key, it's pointless to make someone type yet another
|
|
* passphrase.
|
|
*
|
|
* (Causing multiple keys to become decrypted in one go isn't a
|
|
* security hole in its own right, because the signatures generated by
|
|
* certified and uncertified keys are identical. So an attacker
|
|
* gaining access to an agent containing one encrypted and one
|
|
* cleartext key with the same private half would still be *able* to
|
|
* generate signatures that went with the encrypted one, even if the
|
|
* agent refused to hand them out in response to the most obvious kind
|
|
* of request.)
|
|
*/
|
|
struct PageantPrivateKeySort {
|
|
/*
|
|
* Information used by the sorting criterion for the private key
|
|
* tree.
|
|
*/
|
|
int ssh_version; /* 1 or 2; primary sort key */
|
|
ptrlen base_pub; /* secondary sort key; never includes a certificate */
|
|
};
|
|
static int privkey_cmpfn(void *av, void *bv)
|
|
{
|
|
PageantPrivateKeySort *a = (PageantPrivateKeySort *)av;
|
|
PageantPrivateKeySort *b = (PageantPrivateKeySort *)bv;
|
|
|
|
if (a->ssh_version != b->ssh_version)
|
|
return a->ssh_version < b->ssh_version ? -1 : +1;
|
|
else
|
|
return ptrlen_strcmp(a->base_pub, b->base_pub);
|
|
}
|
|
|
|
struct PageantPublicKeySort {
|
|
/*
|
|
* Information used by the sorting criterion for the public key
|
|
* tree. Begins with the private key sorting criterion, so that
|
|
* all the public keys sharing a private key appear adjacent in
|
|
* the tree. That's a reasonably sensible order to list them in
|
|
* for the user, and more importantly, it makes it easy to
|
|
* discover when we're deleting the last public key that goes with
|
|
* a particular private one, so as to delete that too. Easier than
|
|
* messing about with fragile reference counts.
|
|
*/
|
|
PageantPrivateKeySort priv;
|
|
ptrlen full_pub; /* may match priv.base_pub, or may include a cert */
|
|
};
|
|
static int pubkey_cmpfn(void *av, void *bv)
|
|
{
|
|
PageantPublicKeySort *a = (PageantPublicKeySort *)av;
|
|
PageantPublicKeySort *b = (PageantPublicKeySort *)bv;
|
|
|
|
int c = privkey_cmpfn(&a->priv, &b->priv);
|
|
if (c)
|
|
return c;
|
|
else
|
|
return ptrlen_strcmp(a->full_pub, b->full_pub);
|
|
}
|
|
|
|
struct PageantPrivateKey {
|
|
PageantPrivateKeySort sort;
|
|
strbuf *base_pub; /* the true owner of sort.base_pub */
|
|
union {
|
|
RSAKey *rkey; /* if sort.priv.ssh_version == 1 */
|
|
ssh_key *skey; /* if sort.priv.ssh_version == 2 */
|
|
};
|
|
strbuf *encrypted_key_file;
|
|
/* encrypted_key_comment stores the comment belonging to the
|
|
* encrypted key file. This is used when presenting deferred
|
|
* decryption prompts, because if the user had encrypted their
|
|
* uncert and cert keys with different passphrases, the passphrase
|
|
* prompt must reliably signal which file they're supposed to be
|
|
* entering the passphrase for. */
|
|
char *encrypted_key_comment;
|
|
bool decryption_prompt_active;
|
|
PageantKeyRequestNode blocked_requests;
|
|
PageantClientDialogId dlgid;
|
|
};
|
|
static tree234 *privkeytree;
|
|
|
|
struct PageantPublicKey {
|
|
PageantPublicKeySort sort;
|
|
strbuf *base_pub; /* the true owner of sort.priv.base_pub */
|
|
strbuf *full_pub; /* the true owner of sort.full_pub */
|
|
char *comment;
|
|
};
|
|
static tree234 *pubkeytree;
|
|
|
|
typedef struct PageantSignOp PageantSignOp;
|
|
struct PageantSignOp {
|
|
PageantPrivateKey *priv;
|
|
strbuf *data_to_sign;
|
|
unsigned flags;
|
|
int crLine;
|
|
unsigned char failure_type;
|
|
|
|
PageantKeyRequestNode pkr;
|
|
PageantAsyncOp pao;
|
|
};
|
|
|
|
/* Master lock that indicates whether a GUI request is currently in
|
|
* progress */
|
|
static bool gui_request_in_progress = false;
|
|
static PageantKeyRequestNode requests_blocked_on_gui =
|
|
{ &requests_blocked_on_gui, &requests_blocked_on_gui };
|
|
|
|
static void failure(PageantClient *pc, PageantClientRequestId *reqid,
|
|
strbuf *sb, unsigned char type, const char *fmt, ...);
|
|
static void fail_requests_for_key(PageantPrivateKey *priv, const char *reason);
|
|
static PageantPublicKey *pageant_nth_pubkey(int ssh_version, int i);
|
|
|
|
static void pk_priv_free(PageantPrivateKey *priv)
|
|
{
|
|
if (priv->base_pub)
|
|
strbuf_free(priv->base_pub);
|
|
if (priv->sort.ssh_version == 1 && priv->rkey) {
|
|
freersakey(priv->rkey);
|
|
sfree(priv->rkey);
|
|
}
|
|
if (priv->sort.ssh_version == 2 && priv->skey) {
|
|
ssh_key_free(priv->skey);
|
|
}
|
|
if (priv->encrypted_key_file)
|
|
strbuf_free(priv->encrypted_key_file);
|
|
if (priv->encrypted_key_comment)
|
|
sfree(priv->encrypted_key_comment);
|
|
fail_requests_for_key(priv, "key deleted from Pageant while signing "
|
|
"request was pending");
|
|
sfree(priv);
|
|
}
|
|
|
|
static void pk_pub_free(PageantPublicKey *pub)
|
|
{
|
|
if (pub->full_pub)
|
|
strbuf_free(pub->full_pub);
|
|
sfree(pub->comment);
|
|
sfree(pub);
|
|
}
|
|
|
|
static strbuf *makeblob1(RSAKey *rkey)
|
|
{
|
|
strbuf *blob = strbuf_new();
|
|
rsa_ssh1_public_blob(BinarySink_UPCAST(blob), rkey,
|
|
RSA_SSH1_EXPONENT_FIRST);
|
|
return blob;
|
|
}
|
|
|
|
static strbuf *makeblob2full(ssh_key *key)
|
|
{
|
|
strbuf *blob = strbuf_new();
|
|
ssh_key_public_blob(key, BinarySink_UPCAST(blob));
|
|
return blob;
|
|
}
|
|
|
|
static strbuf *makeblob2base(ssh_key *key)
|
|
{
|
|
strbuf *blob = strbuf_new();
|
|
ssh_key_public_blob(ssh_key_base_key(key), BinarySink_UPCAST(blob));
|
|
return blob;
|
|
}
|
|
|
|
static PageantPrivateKey *pub_to_priv(PageantPublicKey *pub)
|
|
{
|
|
PageantPrivateKey *priv = find234(privkeytree, &pub->sort.priv, NULL);
|
|
assert(priv && "Public and private trees out of sync!");
|
|
return priv;
|
|
}
|
|
|
|
static PageantPublicKey *findpubkey1(RSAKey *reqkey)
|
|
{
|
|
strbuf *blob = makeblob1(reqkey);
|
|
PageantPublicKeySort sort;
|
|
sort.priv.ssh_version = 1;
|
|
sort.priv.base_pub = ptrlen_from_strbuf(blob);
|
|
sort.full_pub = ptrlen_from_strbuf(blob);
|
|
PageantPublicKey *toret = find234(pubkeytree, &sort, NULL);
|
|
strbuf_free(blob);
|
|
return toret;
|
|
}
|
|
|
|
/*
|
|
* Constructs the base_pub element of a PageantPublicKeySort, starting
|
|
* from full_pub. This may involve allocating a strbuf to store it in,
|
|
* which must survive until after you've finished using the resulting
|
|
* PageantPublicKeySort. Hence, the strbuf (if any) is returned from
|
|
* this function, and if it's non-NULL then the caller must eventually
|
|
* free it.
|
|
*/
|
|
static strbuf *make_base_pub_2(PageantPublicKeySort *sort)
|
|
{
|
|
/* Start with the fallback option of making base_pub equal full_pub */
|
|
sort->priv.base_pub = sort->full_pub;
|
|
|
|
/* Now reconstruct a distinct base_pub without a cert, if possible
|
|
* and necessary */
|
|
strbuf *base_pub = NULL;
|
|
BinarySource src[1];
|
|
BinarySource_BARE_INIT_PL(src, sort->full_pub);
|
|
ptrlen algname = get_string(src);
|
|
const ssh_keyalg *alg = find_pubkey_alg_len(algname);
|
|
if (alg && alg->is_certificate) {
|
|
ssh_key *key = ssh_key_new_pub(alg, sort->full_pub);
|
|
if (key) {
|
|
base_pub = strbuf_new();
|
|
ssh_key_public_blob(ssh_key_base_key(key),
|
|
BinarySink_UPCAST(base_pub));
|
|
sort->priv.base_pub = ptrlen_from_strbuf(base_pub);
|
|
ssh_key_free(key);
|
|
}
|
|
}
|
|
|
|
return base_pub; /* caller must free once they're done with sort */
|
|
}
|
|
|
|
static PageantPublicKey *findpubkey2(ptrlen full_pub)
|
|
{
|
|
PageantPublicKeySort sort;
|
|
sort.priv.ssh_version = 2;
|
|
sort.full_pub = full_pub;
|
|
strbuf *base_pub = make_base_pub_2(&sort);
|
|
PageantPublicKey *toret = find234(pubkeytree, &sort, NULL);
|
|
if (base_pub)
|
|
strbuf_free(base_pub);
|
|
return toret;
|
|
}
|
|
|
|
static int find_first_pubkey_for_version(int ssh_version)
|
|
{
|
|
PageantPublicKeySort sort;
|
|
sort.priv.ssh_version = ssh_version;
|
|
sort.priv.base_pub = PTRLEN_LITERAL("");
|
|
sort.full_pub = PTRLEN_LITERAL("");
|
|
int pos;
|
|
if (findrelpos234(pubkeytree, &sort, NULL, REL234_GE, &pos))
|
|
return pos;
|
|
return count234(pubkeytree);
|
|
}
|
|
|
|
static int count_keys(int ssh_version)
|
|
{
|
|
return (find_first_pubkey_for_version(ssh_version + 1) -
|
|
find_first_pubkey_for_version(ssh_version));
|
|
}
|
|
int pageant_count_ssh1_keys(void) { return count_keys(1); }
|
|
int pageant_count_ssh2_keys(void) { return count_keys(2); }
|
|
|
|
/*
|
|
* Common code to add a key to the trees. We fill in as many fields
|
|
* here as we can share between SSH versions: the ptrlens in the
|
|
* sorting field, the whole of pub->sort.priv, and the linked list of
|
|
* blocked requests.
|
|
*/
|
|
static bool pageant_add_key_common(PageantPublicKey *pub,
|
|
PageantPrivateKey *priv)
|
|
{
|
|
int ssh_version = priv->sort.ssh_version;
|
|
|
|
priv->sort.base_pub = ptrlen_from_strbuf(priv->base_pub);
|
|
|
|
pub->base_pub = strbuf_dup(priv->sort.base_pub);
|
|
pub->sort.priv.ssh_version = priv->sort.ssh_version;
|
|
pub->sort.priv.base_pub = ptrlen_from_strbuf(pub->base_pub);
|
|
pub->sort.full_pub = ptrlen_from_strbuf(pub->full_pub);
|
|
priv->blocked_requests.next = priv->blocked_requests.prev =
|
|
&priv->blocked_requests;
|
|
|
|
/*
|
|
* Try to add the private key to privkeytree, or combine new parts
|
|
* of it with what's already there.
|
|
*/
|
|
PageantPrivateKey *priv_in_tree = add234(privkeytree, priv);
|
|
if (priv_in_tree == priv) {
|
|
/* The key wasn't in the tree at all, and we've just added it. */
|
|
} else {
|
|
/* The key was already in the tree, so we'll be freeing priv. */
|
|
|
|
if (ssh_version == 2 && priv->skey && !priv_in_tree->skey) {
|
|
/* The key was only stored encrypted, and now we have an
|
|
* unencrypted version to add to the existing record. */
|
|
priv_in_tree->skey = priv->skey;
|
|
priv->skey = NULL; /* so pk_priv_free won't free it */
|
|
}
|
|
|
|
if (ssh_version == 2 && priv->encrypted_key_file &&
|
|
!priv_in_tree->encrypted_key_file) {
|
|
/* Conversely, the key was only stored in clear, and now
|
|
* we have an encrypted version to add to it. */
|
|
priv_in_tree->encrypted_key_file = priv->encrypted_key_file;
|
|
priv->encrypted_key_file = NULL;
|
|
priv_in_tree->encrypted_key_comment = priv->encrypted_key_comment;
|
|
priv->encrypted_key_comment = NULL;
|
|
}
|
|
|
|
pk_priv_free(priv);
|
|
}
|
|
|
|
/*
|
|
* Try to add the public key.
|
|
*/
|
|
PageantPublicKey *pub_in_tree = add234(pubkeytree, pub);
|
|
if (pub_in_tree == pub) {
|
|
/* Successfully added a new key. */
|
|
return true;
|
|
} else {
|
|
/* This public key was already there. */
|
|
pk_pub_free(pub);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool pageant_add_ssh1_key(RSAKey *rkey)
|
|
{
|
|
PageantPublicKey *pub = snew(PageantPublicKey);
|
|
memset(pub, 0, sizeof(PageantPublicKey));
|
|
PageantPrivateKey *priv = snew(PageantPrivateKey);
|
|
memset(priv, 0, sizeof(PageantPrivateKey));
|
|
|
|
priv->sort.ssh_version = 1;
|
|
priv->base_pub = makeblob1(rkey);
|
|
pub->full_pub = makeblob1(rkey);
|
|
|
|
if (rkey->comment)
|
|
pub->comment = dupstr(rkey->comment);
|
|
|
|
priv->rkey = snew(RSAKey);
|
|
duprsakey(priv->rkey, rkey);
|
|
|
|
return pageant_add_key_common(pub, priv);
|
|
}
|
|
|
|
static bool pageant_add_ssh2_key(ssh2_userkey *skey)
|
|
{
|
|
PageantPublicKey *pub = snew(PageantPublicKey);
|
|
memset(pub, 0, sizeof(PageantPublicKey));
|
|
PageantPrivateKey *priv = snew(PageantPrivateKey);
|
|
memset(priv, 0, sizeof(PageantPrivateKey));
|
|
|
|
priv->sort.ssh_version = 2;
|
|
priv->base_pub = makeblob2base(skey->key);
|
|
pub->full_pub = makeblob2full(skey->key);
|
|
|
|
if (skey->comment)
|
|
pub->comment = dupstr(skey->comment);
|
|
|
|
/* Duplicate the ssh_key to go in priv */
|
|
{
|
|
strbuf *tmp = strbuf_new_nm();
|
|
ssh_key_openssh_blob(skey->key, BinarySink_UPCAST(tmp));
|
|
BinarySource src[1];
|
|
BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(tmp));
|
|
priv->skey = ssh_key_new_priv_openssh(ssh_key_alg(skey->key), src);
|
|
strbuf_free(tmp);
|
|
}
|
|
|
|
return pageant_add_key_common(pub, priv);
|
|
}
|
|
|
|
static bool pageant_add_ssh2_key_encrypted(PageantPublicKeySort sort,
|
|
const char *comment, ptrlen keyfile)
|
|
{
|
|
PageantPublicKey *pub = snew(PageantPublicKey);
|
|
memset(pub, 0, sizeof(PageantPublicKey));
|
|
PageantPrivateKey *priv = snew(PageantPrivateKey);
|
|
memset(priv, 0, sizeof(PageantPrivateKey));
|
|
|
|
assert(sort.priv.ssh_version == 2);
|
|
priv->sort.ssh_version = sort.priv.ssh_version;
|
|
priv->base_pub = strbuf_dup(sort.priv.base_pub);
|
|
pub->full_pub = strbuf_dup(sort.full_pub);
|
|
|
|
pub->comment = dupstr(comment);
|
|
|
|
priv->encrypted_key_file = strbuf_dup_nm(keyfile);
|
|
priv->encrypted_key_comment = dupstr(comment);
|
|
|
|
return pageant_add_key_common(pub, priv);
|
|
}
|
|
|
|
static void remove_pubkey_cleanup(PageantPublicKey *pub)
|
|
{
|
|
/* Common function called when we've just removed a public key
|
|
* from pubkeytree: we must also check whether that was the last
|
|
* public key sharing a private half, and if so, remove the
|
|
* corresponding private entry too. */
|
|
|
|
PageantPublicKeySort pubsearch;
|
|
pubsearch.priv = pub->sort.priv;
|
|
pubsearch.full_pub = PTRLEN_LITERAL("");
|
|
PageantPublicKey *pubfound = findrel234(
|
|
pubkeytree, &pubsearch, NULL, REL234_GE);
|
|
|
|
if (pubfound && !privkey_cmpfn(&pub->sort.priv, &pubfound->sort.priv)) {
|
|
/* There's still a public key which has the same sort.priv as
|
|
* the one we've just removed. We're good. */
|
|
} else {
|
|
/* We've just removed the last public key of the family, so
|
|
* delete the private half as well. */
|
|
PageantPrivateKey *priv = del234(privkeytree, &pub->sort.priv);
|
|
assert(priv);
|
|
assert(!privkey_cmpfn(&priv->sort, &pub->sort.priv));
|
|
pk_priv_free(priv);
|
|
}
|
|
}
|
|
|
|
static PageantPublicKey *del_pubkey_pos(int pos)
|
|
{
|
|
PageantPublicKey *deleted = delpos234(pubkeytree, pos);
|
|
remove_pubkey_cleanup(deleted);
|
|
return deleted;
|
|
}
|
|
|
|
static void del_pubkey(PageantPublicKey *to_delete)
|
|
{
|
|
PageantPublicKey *deleted = del234(pubkeytree, to_delete);
|
|
remove_pubkey_cleanup(deleted);
|
|
}
|
|
|
|
static void remove_all_keys(int ssh_version)
|
|
{
|
|
int start = find_first_pubkey_for_version(ssh_version);
|
|
int end = find_first_pubkey_for_version(ssh_version + 1);
|
|
while (end > start) {
|
|
PageantPublicKey *pub = del_pubkey_pos(--end);
|
|
assert(pub->sort.priv.ssh_version == ssh_version);
|
|
pk_pub_free(pub);
|
|
}
|
|
}
|
|
|
|
static void list_keys(BinarySink *bs, int ssh_version, bool extended)
|
|
{
|
|
int i;
|
|
PageantPublicKey *pub;
|
|
|
|
put_uint32(bs, count_keys(ssh_version));
|
|
for (i = find_first_pubkey_for_version(ssh_version);
|
|
NULL != (pub = index234(pubkeytree, i)); i++) {
|
|
if (pub->sort.priv.ssh_version != ssh_version)
|
|
break;
|
|
|
|
if (ssh_version > 1)
|
|
put_stringpl(bs, pub->sort.full_pub);
|
|
else
|
|
put_datapl(bs, pub->sort.full_pub); /* no header */
|
|
|
|
put_stringpl(bs, ptrlen_from_asciz(pub->comment));
|
|
|
|
if (extended) {
|
|
assert(ssh_version == 2); /* extended lists not supported in v1 */
|
|
|
|
/*
|
|
* Append to each key entry a string containing extension
|
|
* data. This string begins with a flags word, and may in
|
|
* future contain further data if flag bits are set saying
|
|
* that it does. Hence, it's wrapped in a containing
|
|
* string, so that clients that only partially understand
|
|
* it can still find the parts they do understand.
|
|
*/
|
|
PageantPrivateKey *priv = pub_to_priv(pub);
|
|
|
|
strbuf *sb = strbuf_new();
|
|
|
|
uint32_t flags = 0;
|
|
if (!priv->skey)
|
|
flags |= LIST_EXTENDED_FLAG_HAS_NO_CLEARTEXT_KEY;
|
|
if (priv->encrypted_key_file)
|
|
flags |= LIST_EXTENDED_FLAG_HAS_ENCRYPTED_KEY_FILE;
|
|
put_uint32(sb, flags);
|
|
|
|
put_stringsb(bs, sb);
|
|
}
|
|
}
|
|
}
|
|
|
|
void pageant_make_keylist1(BinarySink *bs) { list_keys(bs, 1, false); }
|
|
void pageant_make_keylist2(BinarySink *bs) { list_keys(bs, 2, false); }
|
|
void pageant_make_keylist_extended(BinarySink *bs) { list_keys(bs, 2, true); }
|
|
|
|
void pageant_register_client(PageantClient *pc)
|
|
{
|
|
pc->info = snew(PageantClientInfo);
|
|
pc->info->pc = pc;
|
|
pc->info->head.prev = pc->info->head.next = &pc->info->head;
|
|
}
|
|
|
|
void pageant_unregister_client(PageantClient *pc)
|
|
{
|
|
PageantClientInfo *info = pc->info;
|
|
assert(info);
|
|
assert(info->pc == pc);
|
|
|
|
while (pc->info->head.next != &pc->info->head) {
|
|
PageantAsyncOp *pao = container_of(pc->info->head.next,
|
|
PageantAsyncOp, cr);
|
|
pageant_async_op_unlink_and_free(pao);
|
|
}
|
|
|
|
sfree(pc->info);
|
|
}
|
|
|
|
static PRINTF_LIKE(5, 6) void failure(
|
|
PageantClient *pc, PageantClientRequestId *reqid, strbuf *sb,
|
|
unsigned char type, const char *fmt, ...)
|
|
{
|
|
strbuf_clear(sb);
|
|
put_byte(sb, type);
|
|
if (!pc->suppress_logging) {
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
char *msg = dupvprintf(fmt, ap);
|
|
va_end(ap);
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_FAILURE (%s)", msg);
|
|
sfree(msg);
|
|
}
|
|
}
|
|
|
|
static void signop_link_to_key(PageantSignOp *so)
|
|
{
|
|
assert(!so->pkr.prev);
|
|
assert(!so->pkr.next);
|
|
|
|
so->pkr.prev = so->priv->blocked_requests.prev;
|
|
so->pkr.next = &so->priv->blocked_requests;
|
|
so->pkr.prev->next = &so->pkr;
|
|
so->pkr.next->prev = &so->pkr;
|
|
}
|
|
|
|
static void signop_link_to_pending_gui_request(PageantSignOp *so)
|
|
{
|
|
assert(!so->pkr.prev);
|
|
assert(!so->pkr.next);
|
|
|
|
so->pkr.prev = requests_blocked_on_gui.prev;
|
|
so->pkr.next = &requests_blocked_on_gui;
|
|
so->pkr.prev->next = &so->pkr;
|
|
so->pkr.next->prev = &so->pkr;
|
|
}
|
|
|
|
static void signop_unlink(PageantSignOp *so)
|
|
{
|
|
if (so->pkr.next) {
|
|
assert(so->pkr.prev);
|
|
so->pkr.next->prev = so->pkr.prev;
|
|
so->pkr.prev->next = so->pkr.next;
|
|
so->pkr.prev = so->pkr.next = NULL;
|
|
} else {
|
|
assert(!so->pkr.prev);
|
|
}
|
|
}
|
|
|
|
static void signop_free(PageantAsyncOp *pao)
|
|
{
|
|
PageantSignOp *so = container_of(pao, PageantSignOp, pao);
|
|
strbuf_free(so->data_to_sign);
|
|
sfree(so);
|
|
}
|
|
|
|
static bool request_passphrase(PageantClient *pc, PageantPrivateKey *priv)
|
|
{
|
|
if (!priv->decryption_prompt_active) {
|
|
assert(!gui_request_in_progress);
|
|
|
|
bool created_dlg = pageant_client_ask_passphrase(
|
|
pc, &priv->dlgid, priv->encrypted_key_comment);
|
|
|
|
if (!created_dlg)
|
|
return false;
|
|
|
|
gui_request_in_progress = true;
|
|
priv->decryption_prompt_active = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void signop_coroutine(PageantAsyncOp *pao)
|
|
{
|
|
PageantSignOp *so = container_of(pao, PageantSignOp, pao);
|
|
strbuf *response;
|
|
|
|
crBegin(so->crLine);
|
|
|
|
while (!so->priv->skey && gui_request_in_progress) {
|
|
signop_link_to_pending_gui_request(so);
|
|
crReturnV;
|
|
signop_unlink(so);
|
|
}
|
|
|
|
if (!so->priv->skey) {
|
|
assert(so->priv->encrypted_key_file);
|
|
|
|
if (!request_passphrase(so->pao.info->pc, so->priv)) {
|
|
response = strbuf_new();
|
|
failure(so->pao.info->pc, so->pao.reqid, response,
|
|
so->failure_type, "on-demand decryption could not "
|
|
"prompt for a passphrase");
|
|
goto respond;
|
|
}
|
|
|
|
signop_link_to_key(so);
|
|
crReturnV;
|
|
signop_unlink(so);
|
|
}
|
|
|
|
uint32_t supported_flags = ssh_key_supported_flags(so->priv->skey);
|
|
if (so->flags & ~supported_flags) {
|
|
/*
|
|
* We MUST reject any message containing flags we don't
|
|
* understand.
|
|
*/
|
|
response = strbuf_new();
|
|
failure(so->pao.info->pc, so->pao.reqid, response, so->failure_type,
|
|
"unsupported flag bits 0x%08"PRIx32,
|
|
so->flags & ~supported_flags);
|
|
goto respond;
|
|
}
|
|
|
|
char *invalid = ssh_key_invalid(so->priv->skey, so->flags);
|
|
if (invalid) {
|
|
response = strbuf_new();
|
|
failure(so->pao.info->pc, so->pao.reqid, response, so->failure_type,
|
|
"key invalid: %s", invalid);
|
|
sfree(invalid);
|
|
goto respond;
|
|
}
|
|
|
|
strbuf *signature = strbuf_new();
|
|
ssh_key_sign(so->priv->skey, ptrlen_from_strbuf(so->data_to_sign),
|
|
so->flags, BinarySink_UPCAST(signature));
|
|
|
|
response = strbuf_new();
|
|
put_byte(response, SSH2_AGENT_SIGN_RESPONSE);
|
|
put_stringsb(response, signature);
|
|
|
|
respond:
|
|
pageant_client_got_response(so->pao.info->pc, so->pao.reqid,
|
|
ptrlen_from_strbuf(response));
|
|
strbuf_free(response);
|
|
|
|
pageant_async_op_unlink_and_free(&so->pao);
|
|
crFinishFreedV;
|
|
}
|
|
|
|
static const PageantAsyncOpVtable signop_vtable = {
|
|
.coroutine = signop_coroutine,
|
|
.free = signop_free,
|
|
};
|
|
|
|
static void fail_requests_for_key(PageantPrivateKey *priv, const char *reason)
|
|
{
|
|
while (priv->blocked_requests.next != &priv->blocked_requests) {
|
|
PageantSignOp *so = container_of(priv->blocked_requests.next,
|
|
PageantSignOp, pkr);
|
|
signop_unlink(so);
|
|
strbuf *sb = strbuf_new();
|
|
failure(so->pao.info->pc, so->pao.reqid, sb, so->failure_type,
|
|
"%s", reason);
|
|
pageant_client_got_response(so->pao.info->pc, so->pao.reqid,
|
|
ptrlen_from_strbuf(sb));
|
|
strbuf_free(sb);
|
|
pageant_async_op_unlink_and_free(&so->pao);
|
|
}
|
|
}
|
|
|
|
static void unblock_requests_for_key(PageantPrivateKey *priv)
|
|
{
|
|
for (PageantKeyRequestNode *pkr = priv->blocked_requests.next;
|
|
pkr != &priv->blocked_requests; pkr = pkr->next) {
|
|
PageantSignOp *so = container_of(pkr, PageantSignOp, pkr);
|
|
queue_toplevel_callback(pageant_async_op_callback, &so->pao);
|
|
}
|
|
}
|
|
|
|
static void unblock_pending_gui_requests(void)
|
|
{
|
|
for (PageantKeyRequestNode *pkr = requests_blocked_on_gui.next;
|
|
pkr != &requests_blocked_on_gui; pkr = pkr->next) {
|
|
PageantSignOp *so = container_of(pkr, PageantSignOp, pkr);
|
|
queue_toplevel_callback(pageant_async_op_callback, &so->pao);
|
|
}
|
|
}
|
|
|
|
void pageant_passphrase_request_success(PageantClientDialogId *dlgid,
|
|
ptrlen passphrase)
|
|
{
|
|
PageantPrivateKey *priv = container_of(dlgid, PageantPrivateKey, dlgid);
|
|
|
|
assert(gui_request_in_progress);
|
|
gui_request_in_progress = false;
|
|
priv->decryption_prompt_active = false;
|
|
|
|
if (!priv->skey) {
|
|
const char *error;
|
|
|
|
BinarySource src[1];
|
|
BinarySource_BARE_INIT_PL(src, ptrlen_from_strbuf(
|
|
priv->encrypted_key_file));
|
|
|
|
strbuf *ppsb = strbuf_dup_nm(passphrase);
|
|
ssh2_userkey *skey = ppk_load_s(src, ppsb->s, &error);
|
|
strbuf_free(ppsb);
|
|
|
|
if (!skey) {
|
|
fail_requests_for_key(priv, "unable to decrypt key");
|
|
return;
|
|
} else if (skey == SSH2_WRONG_PASSPHRASE) {
|
|
/*
|
|
* Find a PageantClient to use for another attempt at
|
|
* request_passphrase.
|
|
*/
|
|
PageantKeyRequestNode *pkr = priv->blocked_requests.next;
|
|
if (pkr == &priv->blocked_requests) {
|
|
/*
|
|
* Special case: if all the requests have gone away at
|
|
* this point, we need not bother putting up a request
|
|
* at all any more.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
PageantSignOp *so = container_of(priv->blocked_requests.next,
|
|
PageantSignOp, pkr);
|
|
|
|
priv->decryption_prompt_active = false;
|
|
if (!request_passphrase(so->pao.info->pc, so->priv)) {
|
|
fail_requests_for_key(priv, "unable to continue creating "
|
|
"passphrase prompts");
|
|
}
|
|
return;
|
|
} else {
|
|
priv->skey = skey->key;
|
|
sfree(skey->comment);
|
|
sfree(skey);
|
|
keylist_update();
|
|
}
|
|
}
|
|
|
|
unblock_requests_for_key(priv);
|
|
|
|
unblock_pending_gui_requests();
|
|
}
|
|
|
|
void pageant_passphrase_request_refused(PageantClientDialogId *dlgid)
|
|
{
|
|
PageantPrivateKey *priv = container_of(dlgid, PageantPrivateKey, dlgid);
|
|
|
|
assert(gui_request_in_progress);
|
|
gui_request_in_progress = false;
|
|
priv->decryption_prompt_active = false;
|
|
|
|
fail_requests_for_key(priv, "user refused to supply passphrase");
|
|
|
|
unblock_pending_gui_requests();
|
|
}
|
|
|
|
typedef struct PageantImmOp PageantImmOp;
|
|
struct PageantImmOp {
|
|
int crLine;
|
|
strbuf *response;
|
|
|
|
PageantAsyncOp pao;
|
|
};
|
|
|
|
static void immop_free(PageantAsyncOp *pao)
|
|
{
|
|
PageantImmOp *io = container_of(pao, PageantImmOp, pao);
|
|
if (io->response)
|
|
strbuf_free(io->response);
|
|
sfree(io);
|
|
}
|
|
|
|
static void immop_coroutine(PageantAsyncOp *pao)
|
|
{
|
|
PageantImmOp *io = container_of(pao, PageantImmOp, pao);
|
|
|
|
crBegin(io->crLine);
|
|
|
|
if (0) crReturnV;
|
|
|
|
pageant_client_got_response(io->pao.info->pc, io->pao.reqid,
|
|
ptrlen_from_strbuf(io->response));
|
|
pageant_async_op_unlink_and_free(&io->pao);
|
|
crFinishFreedV;
|
|
}
|
|
|
|
static const PageantAsyncOpVtable immop_vtable = {
|
|
.coroutine = immop_coroutine,
|
|
.free = immop_free,
|
|
};
|
|
|
|
static bool reencrypt_key(PageantPublicKey *pub)
|
|
{
|
|
PageantPrivateKey *priv = pub_to_priv(pub);
|
|
|
|
if (priv->sort.ssh_version != 2) {
|
|
/*
|
|
* We don't support storing SSH-1 keys in encrypted form at
|
|
* all.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
if (!priv->encrypted_key_file) {
|
|
/*
|
|
* We can't re-encrypt a key if it doesn't have an encrypted
|
|
* form. (We could make one up, of course - but with what
|
|
* passphrase that we could expect the user to know later?)
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
/* Only actually free priv->skey if it exists. But we return success
|
|
* regardless, so that 'please ensure this key isn't stored
|
|
* decrypted' is idempotent. */
|
|
if (priv->skey) {
|
|
ssh_key_free(priv->skey);
|
|
priv->skey = NULL;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#define DECL_EXT_ENUM(id, name) id,
|
|
enum Extension { KNOWN_EXTENSIONS(DECL_EXT_ENUM) EXT_UNKNOWN };
|
|
#define DEF_EXT_NAMES(id, name) PTRLEN_DECL_LITERAL(name),
|
|
static const ptrlen extension_names[] = { KNOWN_EXTENSIONS(DEF_EXT_NAMES) };
|
|
|
|
static PageantAsyncOp *pageant_make_op(
|
|
PageantClient *pc, PageantClientRequestId *reqid, ptrlen msgpl)
|
|
{
|
|
BinarySource msg[1];
|
|
strbuf *sb = strbuf_new_nm();
|
|
unsigned char failure_type = SSH_AGENT_FAILURE;
|
|
int type;
|
|
|
|
#define fail(...) failure(pc, reqid, sb, failure_type, __VA_ARGS__)
|
|
|
|
BinarySource_BARE_INIT_PL(msg, msgpl);
|
|
|
|
type = get_byte(msg);
|
|
if (get_err(msg)) {
|
|
fail("message contained no type code");
|
|
goto responded;
|
|
}
|
|
|
|
switch (type) {
|
|
case SSH1_AGENTC_REQUEST_RSA_IDENTITIES: {
|
|
/*
|
|
* Reply with SSH1_AGENT_RSA_IDENTITIES_ANSWER.
|
|
*/
|
|
pageant_client_log(pc, reqid,
|
|
"request: SSH1_AGENTC_REQUEST_RSA_IDENTITIES");
|
|
|
|
put_byte(sb, SSH1_AGENT_RSA_IDENTITIES_ANSWER);
|
|
pageant_make_keylist1(BinarySink_UPCAST(sb));
|
|
|
|
pageant_client_log(pc, reqid,
|
|
"reply: SSH1_AGENT_RSA_IDENTITIES_ANSWER");
|
|
if (!pc->suppress_logging) {
|
|
int i;
|
|
PageantPublicKey *pub;
|
|
for (i = 0; NULL != (pub = pageant_nth_pubkey(1, i)); i++) {
|
|
PageantPrivateKey *priv = pub_to_priv(pub);
|
|
char *fingerprint = rsa_ssh1_fingerprint(priv->rkey);
|
|
pageant_client_log(pc, reqid, "returned key: %s",
|
|
fingerprint);
|
|
sfree(fingerprint);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SSH2_AGENTC_REQUEST_IDENTITIES: {
|
|
/*
|
|
* Reply with SSH2_AGENT_IDENTITIES_ANSWER.
|
|
*/
|
|
pageant_client_log(pc, reqid,
|
|
"request: SSH2_AGENTC_REQUEST_IDENTITIES");
|
|
|
|
put_byte(sb, SSH2_AGENT_IDENTITIES_ANSWER);
|
|
pageant_make_keylist2(BinarySink_UPCAST(sb));
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH2_AGENT_IDENTITIES_ANSWER");
|
|
if (!pc->suppress_logging) {
|
|
int i;
|
|
PageantPublicKey *pub;
|
|
for (i = 0; NULL != (pub = pageant_nth_pubkey(2, i)); i++) {
|
|
char *fingerprint = ssh2_double_fingerprint_blob(
|
|
pub->sort.full_pub, SSH_FPTYPE_DEFAULT);
|
|
pageant_client_log(pc, reqid, "returned key: %s %s",
|
|
fingerprint, pub->comment);
|
|
sfree(fingerprint);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SSH1_AGENTC_RSA_CHALLENGE: {
|
|
/*
|
|
* Reply with either SSH1_AGENT_RSA_RESPONSE or
|
|
* SSH_AGENT_FAILURE, depending on whether we have that key
|
|
* or not.
|
|
*/
|
|
RSAKey reqkey;
|
|
PageantPublicKey *pub;
|
|
PageantPrivateKey *priv;
|
|
mp_int *challenge, *response;
|
|
ptrlen session_id;
|
|
unsigned response_type;
|
|
unsigned char response_md5[16];
|
|
int i;
|
|
|
|
pageant_client_log(pc, reqid, "request: SSH1_AGENTC_RSA_CHALLENGE");
|
|
|
|
response = NULL;
|
|
memset(&reqkey, 0, sizeof(reqkey));
|
|
|
|
get_rsa_ssh1_pub(msg, &reqkey, RSA_SSH1_EXPONENT_FIRST);
|
|
challenge = get_mp_ssh1(msg);
|
|
session_id = get_data(msg, 16);
|
|
response_type = get_uint32(msg);
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
goto challenge1_cleanup;
|
|
}
|
|
if (response_type != 1) {
|
|
fail("response type other than 1 not supported");
|
|
goto challenge1_cleanup;
|
|
}
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint;
|
|
reqkey.comment = NULL;
|
|
fingerprint = rsa_ssh1_fingerprint(&reqkey);
|
|
pageant_client_log(pc, reqid, "requested key: %s", fingerprint);
|
|
sfree(fingerprint);
|
|
}
|
|
|
|
if ((pub = findpubkey1(&reqkey)) == NULL) {
|
|
fail("key not found");
|
|
goto challenge1_cleanup;
|
|
}
|
|
priv = pub_to_priv(pub);
|
|
response = rsa_ssh1_decrypt(challenge, priv->rkey);
|
|
|
|
{
|
|
ssh_hash *h = ssh_hash_new(&ssh_md5);
|
|
for (i = 0; i < 32; i++)
|
|
put_byte(h, mp_get_byte(response, 31 - i));
|
|
put_datapl(h, session_id);
|
|
ssh_hash_final(h, response_md5);
|
|
}
|
|
|
|
put_byte(sb, SSH1_AGENT_RSA_RESPONSE);
|
|
put_data(sb, response_md5, 16);
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH1_AGENT_RSA_RESPONSE");
|
|
|
|
challenge1_cleanup:
|
|
if (response)
|
|
mp_free(response);
|
|
mp_free(challenge);
|
|
freersakey(&reqkey);
|
|
break;
|
|
}
|
|
case SSH2_AGENTC_SIGN_REQUEST: {
|
|
/*
|
|
* Reply with either SSH2_AGENT_SIGN_RESPONSE or
|
|
* SSH_AGENT_FAILURE, depending on whether we have that key
|
|
* or not.
|
|
*/
|
|
PageantPublicKey *pub;
|
|
ptrlen keyblob, sigdata;
|
|
uint32_t flags;
|
|
|
|
pageant_client_log(pc, reqid, "request: SSH2_AGENTC_SIGN_REQUEST");
|
|
|
|
keyblob = get_string(msg);
|
|
sigdata = get_string(msg);
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
goto responded;
|
|
}
|
|
|
|
/*
|
|
* Later versions of the agent protocol added a flags word
|
|
* on the end of the sign request. That hasn't always been
|
|
* there, so we don't complain if we don't find it.
|
|
*
|
|
* get_uint32 will default to returning zero if no data is
|
|
* available.
|
|
*/
|
|
bool have_flags = false;
|
|
flags = get_uint32(msg);
|
|
if (!get_err(msg))
|
|
have_flags = true;
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint = ssh2_double_fingerprint_blob(
|
|
keyblob, SSH_FPTYPE_DEFAULT);
|
|
pageant_client_log(pc, reqid, "requested key: %s", fingerprint);
|
|
sfree(fingerprint);
|
|
}
|
|
if ((pub = findpubkey2(keyblob)) == NULL) {
|
|
fail("key not found");
|
|
goto responded;
|
|
}
|
|
|
|
if (have_flags)
|
|
pageant_client_log(pc, reqid, "signature flags = 0x%08"PRIx32,
|
|
flags);
|
|
else
|
|
pageant_client_log(pc, reqid, "no signature flags");
|
|
|
|
strbuf_free(sb); /* no immediate response */
|
|
|
|
PageantSignOp *so = snew(PageantSignOp);
|
|
so->pao.vt = &signop_vtable;
|
|
so->pao.info = pc->info;
|
|
so->pao.cr.prev = pc->info->head.prev;
|
|
so->pao.cr.next = &pc->info->head;
|
|
so->pao.cr.prev->next = so->pao.cr.next->prev = &so->pao.cr;
|
|
so->pao.reqid = reqid;
|
|
so->priv = pub_to_priv(pub);
|
|
so->pkr.prev = so->pkr.next = NULL;
|
|
so->data_to_sign = strbuf_dup(sigdata);
|
|
so->flags = flags;
|
|
so->failure_type = failure_type;
|
|
so->crLine = 0;
|
|
return &so->pao;
|
|
break;
|
|
}
|
|
case SSH1_AGENTC_ADD_RSA_IDENTITY: {
|
|
/*
|
|
* Add to the list and return SSH_AGENT_SUCCESS, or
|
|
* SSH_AGENT_FAILURE if the key was malformed.
|
|
*/
|
|
RSAKey *key;
|
|
|
|
pageant_client_log(pc, reqid, "request: SSH1_AGENTC_ADD_RSA_IDENTITY");
|
|
|
|
key = get_rsa_ssh1_priv_agent(msg);
|
|
key->comment = mkstr(get_string(msg));
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
goto add1_cleanup;
|
|
}
|
|
|
|
if (!rsa_verify(key)) {
|
|
fail("key is invalid");
|
|
goto add1_cleanup;
|
|
}
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint = rsa_ssh1_fingerprint(key);
|
|
pageant_client_log(pc, reqid,
|
|
"submitted key: %s", fingerprint);
|
|
sfree(fingerprint);
|
|
}
|
|
|
|
if (pageant_add_ssh1_key(key)) {
|
|
keylist_update();
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
key = NULL; /* don't free it in cleanup */
|
|
} else {
|
|
fail("key already present");
|
|
}
|
|
|
|
add1_cleanup:
|
|
if (key) {
|
|
freersakey(key);
|
|
sfree(key);
|
|
}
|
|
break;
|
|
}
|
|
case SSH2_AGENTC_ADD_IDENTITY: {
|
|
/*
|
|
* Add to the list and return SSH_AGENT_SUCCESS, or
|
|
* SSH_AGENT_FAILURE if the key was malformed.
|
|
*/
|
|
ssh2_userkey *key = NULL;
|
|
ptrlen algpl;
|
|
const ssh_keyalg *alg;
|
|
|
|
pageant_client_log(pc, reqid, "request: SSH2_AGENTC_ADD_IDENTITY");
|
|
|
|
algpl = get_string(msg);
|
|
|
|
key = snew(ssh2_userkey);
|
|
key->key = NULL;
|
|
key->comment = NULL;
|
|
alg = find_pubkey_alg_len(algpl);
|
|
if (!alg) {
|
|
fail("algorithm unknown");
|
|
goto add2_cleanup;
|
|
}
|
|
|
|
key->key = ssh_key_new_priv_openssh(alg, msg);
|
|
|
|
if (!key->key) {
|
|
fail("key setup failed");
|
|
goto add2_cleanup;
|
|
}
|
|
|
|
key->comment = mkstr(get_string(msg));
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
goto add2_cleanup;
|
|
}
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint = ssh2_fingerprint(key->key, SSH_FPTYPE_DEFAULT);
|
|
pageant_client_log(pc, reqid, "submitted key: %s %s",
|
|
fingerprint, key->comment);
|
|
sfree(fingerprint);
|
|
}
|
|
|
|
if (pageant_add_ssh2_key(key)) {
|
|
keylist_update();
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
|
|
key = NULL; /* don't clean it up */
|
|
} else {
|
|
fail("key already present");
|
|
}
|
|
|
|
add2_cleanup:
|
|
if (key) {
|
|
if (key->key)
|
|
ssh_key_free(key->key);
|
|
if (key->comment)
|
|
sfree(key->comment);
|
|
sfree(key);
|
|
}
|
|
break;
|
|
}
|
|
case SSH1_AGENTC_REMOVE_RSA_IDENTITY: {
|
|
/*
|
|
* Remove from the list and return SSH_AGENT_SUCCESS, or
|
|
* perhaps SSH_AGENT_FAILURE if it wasn't in the list to
|
|
* start with.
|
|
*/
|
|
RSAKey reqkey;
|
|
PageantPublicKey *pub;
|
|
|
|
pageant_client_log(pc, reqid,
|
|
"request: SSH1_AGENTC_REMOVE_RSA_IDENTITY");
|
|
|
|
memset(&reqkey, 0, sizeof(reqkey));
|
|
get_rsa_ssh1_pub(msg, &reqkey, RSA_SSH1_EXPONENT_FIRST);
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
freersakey(&reqkey);
|
|
goto responded;
|
|
}
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint;
|
|
reqkey.comment = NULL;
|
|
fingerprint = rsa_ssh1_fingerprint(&reqkey);
|
|
pageant_client_log(pc, reqid, "unwanted key: %s", fingerprint);
|
|
sfree(fingerprint);
|
|
}
|
|
|
|
pub = findpubkey1(&reqkey);
|
|
freersakey(&reqkey);
|
|
if (pub) {
|
|
pageant_client_log(pc, reqid, "found with comment: %s",
|
|
pub->comment);
|
|
|
|
del_pubkey(pub);
|
|
keylist_update();
|
|
pk_pub_free(pub);
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
} else {
|
|
fail("key not found");
|
|
}
|
|
break;
|
|
}
|
|
case SSH2_AGENTC_REMOVE_IDENTITY: {
|
|
/*
|
|
* Remove from the list and return SSH_AGENT_SUCCESS, or
|
|
* perhaps SSH_AGENT_FAILURE if it wasn't in the list to
|
|
* start with.
|
|
*/
|
|
PageantPublicKey *pub;
|
|
ptrlen blob;
|
|
|
|
pageant_client_log(pc, reqid, "request: SSH2_AGENTC_REMOVE_IDENTITY");
|
|
|
|
blob = get_string(msg);
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
goto responded;
|
|
}
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint = ssh2_double_fingerprint_blob(
|
|
blob, SSH_FPTYPE_DEFAULT);
|
|
pageant_client_log(pc, reqid, "unwanted key: %s", fingerprint);
|
|
sfree(fingerprint);
|
|
}
|
|
|
|
pub = findpubkey2(blob);
|
|
if (!pub) {
|
|
fail("key not found");
|
|
goto responded;
|
|
}
|
|
|
|
pageant_client_log(pc, reqid, "found with comment: %s", pub->comment);
|
|
|
|
del_pubkey(pub);
|
|
keylist_update();
|
|
pk_pub_free(pub);
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
break;
|
|
}
|
|
case SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES: {
|
|
/*
|
|
* Remove all SSH-1 keys. Always returns success.
|
|
*/
|
|
pageant_client_log(pc, reqid,
|
|
"request: SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES");
|
|
|
|
remove_all_keys(1);
|
|
keylist_update();
|
|
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
break;
|
|
}
|
|
case SSH2_AGENTC_REMOVE_ALL_IDENTITIES: {
|
|
/*
|
|
* Remove all SSH-2 keys. Always returns success.
|
|
*/
|
|
pageant_client_log(pc, reqid,
|
|
"request: SSH2_AGENTC_REMOVE_ALL_IDENTITIES");
|
|
|
|
remove_all_keys(2);
|
|
keylist_update();
|
|
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
break;
|
|
}
|
|
case SSH2_AGENTC_EXTENSION: {
|
|
enum Extension exttype = EXT_UNKNOWN;
|
|
ptrlen extname = get_string(msg);
|
|
pageant_client_log(pc, reqid,
|
|
"request: SSH2_AGENTC_EXTENSION \"%.*s\"",
|
|
PTRLEN_PRINTF(extname));
|
|
|
|
for (size_t i = 0; i < lenof(extension_names); i++)
|
|
if (ptrlen_eq_ptrlen(extname, extension_names[i])) {
|
|
exttype = i;
|
|
|
|
/*
|
|
* For SSH_AGENTC_EXTENSION requests, the message
|
|
* code SSH_AGENT_FAILURE is reserved for "I don't
|
|
* recognise this extension name at all". For any
|
|
* other kind of failure while processing an
|
|
* extension we _do_ recognise, we must switch to
|
|
* returning a different failure code, with
|
|
* semantics "I understood the extension name, but
|
|
* something else went wrong".
|
|
*/
|
|
failure_type = SSH_AGENT_EXTENSION_FAILURE;
|
|
break;
|
|
}
|
|
|
|
switch (exttype) {
|
|
case EXT_UNKNOWN:
|
|
fail("unrecognised extension name '%.*s'",
|
|
PTRLEN_PRINTF(extname));
|
|
break;
|
|
|
|
case EXT_QUERY:
|
|
/* Standard request to list the supported extensions. */
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
for (size_t i = 0; i < lenof(extension_names); i++)
|
|
put_stringpl(sb, extension_names[i]);
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS + names");
|
|
break;
|
|
|
|
case EXT_ADD_PPK: {
|
|
ptrlen keyfile = get_string(msg);
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
goto responded;
|
|
}
|
|
|
|
strbuf *base_pub = NULL;
|
|
strbuf *full_pub = NULL;
|
|
BinarySource src[1];
|
|
const char *error;
|
|
|
|
full_pub = strbuf_new();
|
|
char *comment;
|
|
|
|
BinarySource_BARE_INIT_PL(src, keyfile);
|
|
if (!ppk_loadpub_s(src, NULL, BinarySink_UPCAST(full_pub),
|
|
&comment, &error)) {
|
|
fail("failed to extract public key blob: %s", error);
|
|
goto add_ppk_cleanup;
|
|
}
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint = ssh2_double_fingerprint_blob(
|
|
ptrlen_from_strbuf(full_pub), SSH_FPTYPE_DEFAULT);
|
|
pageant_client_log(pc, reqid, "add-ppk: %s %s",
|
|
fingerprint, comment);
|
|
sfree(fingerprint);
|
|
}
|
|
|
|
BinarySource_BARE_INIT_PL(src, keyfile);
|
|
bool encrypted = ppk_encrypted_s(src, NULL);
|
|
|
|
if (!encrypted) {
|
|
/* If the key isn't encrypted, then we should just
|
|
* load and add it in the obvious way. */
|
|
BinarySource_BARE_INIT_PL(src, keyfile);
|
|
ssh2_userkey *skey = ppk_load_s(src, NULL, &error);
|
|
if (!skey) {
|
|
fail("failed to decode private key: %s", error);
|
|
} else if (pageant_add_ssh2_key(skey)) {
|
|
keylist_update();
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS"
|
|
" (loaded unencrypted PPK)");
|
|
} else {
|
|
fail("key already present");
|
|
if (skey->key)
|
|
ssh_key_free(skey->key);
|
|
if (skey->comment)
|
|
sfree(skey->comment);
|
|
sfree(skey);
|
|
}
|
|
goto add_ppk_cleanup;
|
|
}
|
|
|
|
PageantPublicKeySort sort;
|
|
sort.priv.ssh_version = 2;
|
|
sort.full_pub = ptrlen_from_strbuf(full_pub);
|
|
base_pub = make_base_pub_2(&sort);
|
|
|
|
pageant_add_ssh2_key_encrypted(sort, comment, keyfile);
|
|
keylist_update();
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
|
|
add_ppk_cleanup:
|
|
if (full_pub)
|
|
strbuf_free(full_pub);
|
|
if (base_pub)
|
|
strbuf_free(base_pub);
|
|
sfree(comment);
|
|
break;
|
|
}
|
|
|
|
case EXT_REENCRYPT: {
|
|
/*
|
|
* Re-encrypt a single key, in the sense of deleting
|
|
* its unencrypted copy, returning it to the state of
|
|
* only having the encrypted PPK form stored, so that
|
|
* the next attempt to use it will have to re-prompt
|
|
* for the passphrase.
|
|
*/
|
|
ptrlen blob = get_string(msg);
|
|
|
|
if (get_err(msg)) {
|
|
fail("unable to decode request");
|
|
goto responded;
|
|
}
|
|
|
|
if (!pc->suppress_logging) {
|
|
char *fingerprint = ssh2_double_fingerprint_blob(
|
|
blob, SSH_FPTYPE_DEFAULT);
|
|
pageant_client_log(pc, reqid, "key to re-encrypt: %s",
|
|
fingerprint);
|
|
sfree(fingerprint);
|
|
}
|
|
|
|
PageantPublicKey *pub = findpubkey2(blob);
|
|
if (!pub) {
|
|
fail("key not found");
|
|
goto responded;
|
|
}
|
|
|
|
pageant_client_log(pc, reqid,
|
|
"found with comment: %s", pub->comment);
|
|
|
|
if (!reencrypt_key(pub)) {
|
|
fail("this key couldn't be re-encrypted");
|
|
goto responded;
|
|
}
|
|
|
|
keylist_update();
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS");
|
|
break;
|
|
}
|
|
|
|
case EXT_REENCRYPT_ALL: {
|
|
/*
|
|
* Re-encrypt all keys that have an encrypted form
|
|
* stored. Usually, returns success, but with a uint32
|
|
* appended indicating how many keys remain
|
|
* unencrypted. The exception is if there is at least
|
|
* one key in the agent and _no_ key was successfully
|
|
* re-encrypted; in that situation we've done nothing,
|
|
* and the client didn't _want_ us to do nothing, so
|
|
* we return failure.
|
|
*
|
|
* (Rationale: the 'failure' message ought to be
|
|
* atomic, that is, you shouldn't return failure
|
|
* having made a state change.)
|
|
*/
|
|
unsigned nfailures = 0, nsuccesses = 0;
|
|
PageantPublicKey *pub;
|
|
|
|
for (int i = 0; (pub = index234(pubkeytree, i)) != NULL; i++) {
|
|
if (reencrypt_key(pub))
|
|
nsuccesses++;
|
|
else
|
|
nfailures++;
|
|
}
|
|
|
|
if (nsuccesses == 0 && nfailures > 0) {
|
|
fail("no key could be re-encrypted");
|
|
} else {
|
|
keylist_update();
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
put_uint32(sb, nfailures);
|
|
pageant_client_log(pc, reqid, "reply: SSH_AGENT_SUCCESS "
|
|
"(%u keys re-encrypted, %u failures)",
|
|
nsuccesses, nfailures);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case EXT_LIST_EXTENDED: {
|
|
/*
|
|
* Return a key list like SSH2_AGENTC_REQUEST_IDENTITIES,
|
|
* except that each key is annotated with extra
|
|
* information such as whether it's currently encrypted.
|
|
*
|
|
* The return message type is AGENT_SUCCESS with auxiliary
|
|
* data, which is more like other extension messages. I
|
|
* think it would be confusing to reuse IDENTITIES_ANSWER
|
|
* for a reply message with an incompatible format.
|
|
*/
|
|
put_byte(sb, SSH_AGENT_SUCCESS);
|
|
pageant_make_keylist_extended(BinarySink_UPCAST(sb));
|
|
|
|
pageant_client_log(pc, reqid,
|
|
"reply: SSH2_AGENT_SUCCESS + key list");
|
|
if (!pc->suppress_logging) {
|
|
int i;
|
|
PageantPublicKey *pub;
|
|
for (i = 0; NULL != (pub = pageant_nth_pubkey(2, i)); i++) {
|
|
char *fingerprint = ssh2_double_fingerprint_blob(
|
|
ptrlen_from_strbuf(pub->full_pub),
|
|
SSH_FPTYPE_DEFAULT);
|
|
pageant_client_log(pc, reqid, "returned key: %s %s",
|
|
fingerprint, pub->comment);
|
|
sfree(fingerprint);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
pageant_client_log(pc, reqid, "request: unknown message type %d",
|
|
type);
|
|
fail("unrecognised message");
|
|
break;
|
|
}
|
|
|
|
#undef fail
|
|
|
|
responded:;
|
|
|
|
PageantImmOp *io = snew(PageantImmOp);
|
|
io->pao.vt = &immop_vtable;
|
|
io->pao.info = pc->info;
|
|
io->pao.cr.prev = pc->info->head.prev;
|
|
io->pao.cr.next = &pc->info->head;
|
|
io->pao.cr.prev->next = io->pao.cr.next->prev = &io->pao.cr;
|
|
io->pao.reqid = reqid;
|
|
io->response = sb;
|
|
io->crLine = 0;
|
|
return &io->pao;
|
|
}
|
|
|
|
void pageant_handle_msg(PageantClient *pc, PageantClientRequestId *reqid,
|
|
ptrlen msgpl)
|
|
{
|
|
PageantAsyncOp *pao = pageant_make_op(pc, reqid, msgpl);
|
|
queue_toplevel_callback(pageant_async_op_callback, pao);
|
|
}
|
|
|
|
void pageant_init(void)
|
|
{
|
|
pageant_local = true;
|
|
pubkeytree = newtree234(pubkey_cmpfn);
|
|
privkeytree = newtree234(privkey_cmpfn);
|
|
}
|
|
|
|
static PageantPublicKey *pageant_nth_pubkey(int ssh_version, int i)
|
|
{
|
|
PageantPublicKey *pub = index234(
|
|
pubkeytree, find_first_pubkey_for_version(ssh_version) + i);
|
|
if (pub && pub->sort.priv.ssh_version == ssh_version)
|
|
return pub;
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
bool pageant_delete_nth_ssh1_key(int i)
|
|
{
|
|
PageantPublicKey *pub = del_pubkey_pos(
|
|
find_first_pubkey_for_version(1) + i);
|
|
if (!pub)
|
|
return false;
|
|
pk_pub_free(pub);
|
|
return true;
|
|
}
|
|
|
|
bool pageant_delete_nth_ssh2_key(int i)
|
|
{
|
|
PageantPublicKey *pub = del_pubkey_pos(
|
|
find_first_pubkey_for_version(2) + i);
|
|
if (!pub)
|
|
return false;
|
|
pk_pub_free(pub);
|
|
return true;
|
|
}
|
|
|
|
bool pageant_reencrypt_nth_ssh2_key(int i)
|
|
{
|
|
PageantPublicKey *pub = index234(
|
|
pubkeytree, find_first_pubkey_for_version(2) + i);
|
|
if (!pub)
|
|
return false;
|
|
return reencrypt_key(pub);
|
|
}
|
|
|
|
void pageant_delete_all(void)
|
|
{
|
|
remove_all_keys(1);
|
|
remove_all_keys(2);
|
|
}
|
|
|
|
void pageant_reencrypt_all(void)
|
|
{
|
|
PageantPublicKey *pub;
|
|
for (int i = 0; (pub = index234(pubkeytree, i)) != NULL; i++)
|
|
reencrypt_key(pub);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* The agent plug.
|
|
*/
|
|
|
|
/*
|
|
* An extra coroutine macro, specific to this code which is consuming
|
|
* 'const char *data'.
|
|
*/
|
|
#define crGetChar(c) do \
|
|
{ \
|
|
while (len == 0) { \
|
|
*crLine = __LINE__; return; case __LINE__:; \
|
|
} \
|
|
len--; \
|
|
(c) = (unsigned char)*data++; \
|
|
} while (0)
|
|
|
|
struct pageant_conn_queued_response {
|
|
struct pageant_conn_queued_response *next, *prev;
|
|
size_t req_index; /* for indexing requests in log messages */
|
|
strbuf *sb;
|
|
PageantClientRequestId reqid;
|
|
};
|
|
|
|
struct pageant_conn_state {
|
|
Socket *connsock;
|
|
PageantListenerClient *plc;
|
|
unsigned char lenbuf[4], pktbuf[AGENT_MAX_MSGLEN];
|
|
unsigned len, got;
|
|
bool real_packet;
|
|
size_t conn_index; /* for indexing connections in log messages */
|
|
size_t req_index; /* for indexing requests in log messages */
|
|
int crLine; /* for coroutine in pageant_conn_receive */
|
|
|
|
struct pageant_conn_queued_response response_queue;
|
|
|
|
PageantClient pc;
|
|
Plug plug;
|
|
};
|
|
|
|
static void pageant_conn_closing(Plug *plug, PlugCloseType type,
|
|
const char *error_msg)
|
|
{
|
|
struct pageant_conn_state *pc = container_of(
|
|
plug, struct pageant_conn_state, plug);
|
|
if (type != PLUGCLOSE_NORMAL)
|
|
pageant_listener_client_log(pc->plc, "c#%"SIZEu": error: %s",
|
|
pc->conn_index, error_msg);
|
|
else
|
|
pageant_listener_client_log(pc->plc, "c#%"SIZEu": connection closed",
|
|
pc->conn_index);
|
|
sk_close(pc->connsock);
|
|
pageant_unregister_client(&pc->pc);
|
|
sfree(pc);
|
|
}
|
|
|
|
static void pageant_conn_sent(Plug *plug, size_t bufsize)
|
|
{
|
|
/* struct pageant_conn_state *pc = container_of(
|
|
plug, struct pageant_conn_state, plug); */
|
|
|
|
/*
|
|
* We do nothing here, because we expect that there won't be a
|
|
* need to throttle and unthrottle the connection to an agent -
|
|
* clients will typically not send many requests, and will wait
|
|
* until they receive each reply before sending a new request.
|
|
*/
|
|
}
|
|
|
|
static void pageant_conn_log(PageantClient *pc, PageantClientRequestId *reqid,
|
|
const char *fmt, va_list ap)
|
|
{
|
|
struct pageant_conn_state *pcs =
|
|
container_of(pc, struct pageant_conn_state, pc);
|
|
struct pageant_conn_queued_response *qr =
|
|
container_of(reqid, struct pageant_conn_queued_response, reqid);
|
|
|
|
char *formatted = dupvprintf(fmt, ap);
|
|
pageant_listener_client_log(pcs->plc, "c#%"SIZEu",r#%"SIZEu": %s",
|
|
pcs->conn_index, qr->req_index, formatted);
|
|
sfree(formatted);
|
|
}
|
|
|
|
static void pageant_conn_got_response(
|
|
PageantClient *pc, PageantClientRequestId *reqid, ptrlen response)
|
|
{
|
|
struct pageant_conn_state *pcs =
|
|
container_of(pc, struct pageant_conn_state, pc);
|
|
struct pageant_conn_queued_response *qr =
|
|
container_of(reqid, struct pageant_conn_queued_response, reqid);
|
|
|
|
qr->sb = strbuf_new_nm();
|
|
put_stringpl(qr->sb, response);
|
|
|
|
while (pcs->response_queue.next != &pcs->response_queue &&
|
|
pcs->response_queue.next->sb) {
|
|
qr = pcs->response_queue.next;
|
|
sk_write(pcs->connsock, qr->sb->u, qr->sb->len);
|
|
qr->next->prev = qr->prev;
|
|
qr->prev->next = qr->next;
|
|
strbuf_free(qr->sb);
|
|
sfree(qr);
|
|
}
|
|
}
|
|
|
|
static bool pageant_conn_ask_passphrase(
|
|
PageantClient *pc, PageantClientDialogId *dlgid, const char *comment)
|
|
{
|
|
struct pageant_conn_state *pcs =
|
|
container_of(pc, struct pageant_conn_state, pc);
|
|
return pageant_listener_client_ask_passphrase(pcs->plc, dlgid, comment);
|
|
}
|
|
|
|
static const PageantClientVtable pageant_connection_clientvt = {
|
|
.log = pageant_conn_log,
|
|
.got_response = pageant_conn_got_response,
|
|
.ask_passphrase = pageant_conn_ask_passphrase,
|
|
};
|
|
|
|
static void pageant_conn_receive(
|
|
Plug *plug, int urgent, const char *data, size_t len)
|
|
{
|
|
struct pageant_conn_state *pc = container_of(
|
|
plug, struct pageant_conn_state, plug);
|
|
char c;
|
|
|
|
crBegin(pc->crLine);
|
|
|
|
while (len > 0) {
|
|
pc->got = 0;
|
|
while (pc->got < 4) {
|
|
crGetChar(c);
|
|
pc->lenbuf[pc->got++] = c;
|
|
}
|
|
|
|
pc->len = GET_32BIT_MSB_FIRST(pc->lenbuf);
|
|
pc->got = 0;
|
|
pc->real_packet = (pc->len < AGENT_MAX_MSGLEN-4);
|
|
|
|
{
|
|
struct pageant_conn_queued_response *qr =
|
|
snew(struct pageant_conn_queued_response);
|
|
qr->prev = pc->response_queue.prev;
|
|
qr->next = &pc->response_queue;
|
|
qr->prev->next = qr->next->prev = qr;
|
|
qr->sb = NULL;
|
|
qr->req_index = pc->req_index++;
|
|
}
|
|
|
|
if (!pc->real_packet) {
|
|
/*
|
|
* Send failure immediately, before consuming the packet
|
|
* data. That way we notify the client reasonably early
|
|
* even if the data channel has just started spewing
|
|
* nonsense.
|
|
*/
|
|
pageant_client_log(&pc->pc, &pc->response_queue.prev->reqid,
|
|
"early reply: SSH_AGENT_FAILURE "
|
|
"(overlong message, length %u)", pc->len);
|
|
static const unsigned char failure[] = { SSH_AGENT_FAILURE };
|
|
pageant_conn_got_response(&pc->pc, &pc->response_queue.prev->reqid,
|
|
make_ptrlen(failure, lenof(failure)));
|
|
}
|
|
|
|
while (pc->got < pc->len) {
|
|
crGetChar(c);
|
|
if (pc->real_packet)
|
|
pc->pktbuf[pc->got] = c;
|
|
pc->got++;
|
|
}
|
|
|
|
if (pc->real_packet)
|
|
pageant_handle_msg(&pc->pc, &pc->response_queue.prev->reqid,
|
|
make_ptrlen(pc->pktbuf, pc->len));
|
|
}
|
|
|
|
crFinishV;
|
|
}
|
|
|
|
struct pageant_listen_state {
|
|
Socket *listensock;
|
|
PageantListenerClient *plc;
|
|
size_t conn_index; /* for indexing connections in log messages */
|
|
|
|
Plug plug;
|
|
};
|
|
|
|
static void pageant_listen_closing(Plug *plug, PlugCloseType type,
|
|
const char *error_msg)
|
|
{
|
|
struct pageant_listen_state *pl = container_of(
|
|
plug, struct pageant_listen_state, plug);
|
|
if (type != PLUGCLOSE_NORMAL)
|
|
pageant_listener_client_log(pl->plc, "listening socket: error: %s",
|
|
error_msg);
|
|
sk_close(pl->listensock);
|
|
pl->listensock = NULL;
|
|
}
|
|
|
|
static const PlugVtable pageant_connection_plugvt = {
|
|
.closing = pageant_conn_closing,
|
|
.receive = pageant_conn_receive,
|
|
.sent = pageant_conn_sent,
|
|
.log = nullplug_log,
|
|
};
|
|
|
|
static int pageant_listen_accepting(Plug *plug,
|
|
accept_fn_t constructor, accept_ctx_t ctx)
|
|
{
|
|
struct pageant_listen_state *pl = container_of(
|
|
plug, struct pageant_listen_state, plug);
|
|
struct pageant_conn_state *pc;
|
|
const char *err;
|
|
SocketEndpointInfo *peerinfo;
|
|
|
|
pc = snew(struct pageant_conn_state);
|
|
pc->plug.vt = &pageant_connection_plugvt;
|
|
pc->pc.vt = &pageant_connection_clientvt;
|
|
pc->plc = pl->plc;
|
|
pc->response_queue.next = pc->response_queue.prev = &pc->response_queue;
|
|
pc->conn_index = pl->conn_index++;
|
|
pc->req_index = 0;
|
|
pc->crLine = 0;
|
|
|
|
pc->connsock = constructor(ctx, &pc->plug);
|
|
if ((err = sk_socket_error(pc->connsock)) != NULL) {
|
|
sk_close(pc->connsock);
|
|
sfree(pc);
|
|
return 1;
|
|
}
|
|
|
|
sk_set_frozen(pc->connsock, false);
|
|
|
|
peerinfo = sk_peer_info(pc->connsock);
|
|
if (peerinfo && peerinfo->log_text) {
|
|
pageant_listener_client_log(pl->plc,
|
|
"c#%"SIZEu": new connection from %s",
|
|
pc->conn_index, peerinfo->log_text);
|
|
} else {
|
|
pageant_listener_client_log(pl->plc, "c#%"SIZEu": new connection",
|
|
pc->conn_index);
|
|
}
|
|
sk_free_endpoint_info(peerinfo);
|
|
|
|
pageant_register_client(&pc->pc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const PlugVtable pageant_listener_plugvt = {
|
|
.closing = pageant_listen_closing,
|
|
.accepting = pageant_listen_accepting,
|
|
.log = nullplug_log,
|
|
};
|
|
|
|
struct pageant_listen_state *pageant_listener_new(
|
|
Plug **plug, PageantListenerClient *plc)
|
|
{
|
|
struct pageant_listen_state *pl = snew(struct pageant_listen_state);
|
|
pl->plug.vt = &pageant_listener_plugvt;
|
|
pl->plc = plc;
|
|
pl->listensock = NULL;
|
|
pl->conn_index = 0;
|
|
*plug = &pl->plug;
|
|
return pl;
|
|
}
|
|
|
|
void pageant_listener_got_socket(struct pageant_listen_state *pl, Socket *sock)
|
|
{
|
|
pl->listensock = sock;
|
|
}
|
|
|
|
void pageant_listener_free(struct pageant_listen_state *pl)
|
|
{
|
|
if (pl->listensock)
|
|
sk_close(pl->listensock);
|
|
sfree(pl);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Code to perform agent operations either as a client, or within the
|
|
* same process as the running agent.
|
|
*/
|
|
|
|
static tree234 *passphrases = NULL;
|
|
|
|
typedef struct PageantInternalClient {
|
|
strbuf *response;
|
|
bool got_response;
|
|
PageantClient pc;
|
|
} PageantInternalClient;
|
|
|
|
static void internal_client_got_response(
|
|
PageantClient *pc, PageantClientRequestId *reqid, ptrlen response)
|
|
{
|
|
PageantInternalClient *pic = container_of(pc, PageantInternalClient, pc);
|
|
strbuf_clear(pic->response);
|
|
put_datapl(pic->response, response);
|
|
pic->got_response = true;
|
|
}
|
|
|
|
static bool internal_client_ask_passphrase(
|
|
PageantClient *pc, PageantClientDialogId *dlgid, const char *comment)
|
|
{
|
|
/* No delaying operations are permitted in this mode */
|
|
return false;
|
|
}
|
|
|
|
static const PageantClientVtable internal_clientvt = {
|
|
.log = NULL,
|
|
.got_response = internal_client_got_response,
|
|
.ask_passphrase = internal_client_ask_passphrase,
|
|
};
|
|
|
|
typedef struct PageantClientOp {
|
|
strbuf *buf;
|
|
bool request_made;
|
|
BinarySink_DELEGATE_IMPLEMENTATION;
|
|
BinarySource_IMPLEMENTATION;
|
|
} PageantClientOp;
|
|
|
|
static PageantClientOp *pageant_client_op_new(void)
|
|
{
|
|
PageantClientOp *pco = snew(PageantClientOp);
|
|
pco->buf = strbuf_new_for_agent_query();
|
|
pco->request_made = false;
|
|
BinarySink_DELEGATE_INIT(pco, pco->buf);
|
|
BinarySource_INIT(pco, "", 0);
|
|
return pco;
|
|
}
|
|
|
|
static void pageant_client_op_free(PageantClientOp *pco)
|
|
{
|
|
if (pco->buf)
|
|
strbuf_free(pco->buf);
|
|
sfree(pco);
|
|
}
|
|
|
|
static unsigned pageant_client_op_query(PageantClientOp *pco)
|
|
{
|
|
/* Since we use the same strbuf for the request and the response,
|
|
* check by assertion that we aren't embarrassingly sending a
|
|
* previous response back to the agent */
|
|
assert(!pco->request_made);
|
|
pco->request_made = true;
|
|
|
|
if (!pageant_local) {
|
|
void *response_raw;
|
|
int resplen_raw;
|
|
agent_query_synchronous(pco->buf, &response_raw, &resplen_raw);
|
|
strbuf_clear(pco->buf);
|
|
put_data(pco->buf, response_raw, resplen_raw);
|
|
sfree(response_raw);
|
|
|
|
/* The data coming back from agent_query_synchronous will have
|
|
* its length field prepended. So we start by parsing it as an
|
|
* SSH-formatted string, and then reinitialise our
|
|
* BinarySource with the interior of that string. */
|
|
BinarySource_INIT_PL(pco, ptrlen_from_strbuf(pco->buf));
|
|
BinarySource_INIT_PL(pco, get_string(pco));
|
|
} else {
|
|
PageantInternalClient pic;
|
|
PageantClientRequestId reqid;
|
|
|
|
pic.pc.vt = &internal_clientvt;
|
|
pic.pc.suppress_logging = true;
|
|
pic.response = pco->buf;
|
|
pic.got_response = false;
|
|
pageant_register_client(&pic.pc);
|
|
|
|
assert(pco->buf->len > 4);
|
|
PageantAsyncOp *pao = pageant_make_op(
|
|
&pic.pc, &reqid, make_ptrlen(pco->buf->s + 4, pco->buf->len - 4));
|
|
while (!pic.got_response)
|
|
pageant_async_op_coroutine(pao);
|
|
|
|
pageant_unregister_client(&pic.pc);
|
|
|
|
BinarySource_INIT_PL(pco, ptrlen_from_strbuf(pco->buf));
|
|
}
|
|
|
|
/* Strip off and directly return the type byte, which every client
|
|
* will need, to save a boilerplate get_byte at each call site */
|
|
unsigned reply_type = get_byte(pco);
|
|
if (get_err(pco))
|
|
reply_type = 256; /* out-of-range code */
|
|
return reply_type;
|
|
}
|
|
|
|
/*
|
|
* After processing a list of filenames, we want to forget the
|
|
* passphrases.
|
|
*/
|
|
void pageant_forget_passphrases(void)
|
|
{
|
|
if (!passphrases) /* in case we never set it up at all */
|
|
return;
|
|
|
|
while (count234(passphrases) > 0) {
|
|
char *pp = index234(passphrases, 0);
|
|
smemclr(pp, strlen(pp));
|
|
delpos234(passphrases, 0);
|
|
sfree(pp);
|
|
}
|
|
}
|
|
|
|
typedef struct KeyListEntry {
|
|
ptrlen blob, comment;
|
|
uint32_t flags;
|
|
} KeyListEntry;
|
|
typedef struct KeyList {
|
|
strbuf *raw_data;
|
|
KeyListEntry *keys;
|
|
size_t nkeys;
|
|
bool broken;
|
|
} KeyList;
|
|
|
|
static void keylist_free(KeyList *kl)
|
|
{
|
|
sfree(kl->keys);
|
|
strbuf_free(kl->raw_data);
|
|
sfree(kl);
|
|
}
|
|
|
|
static PageantClientOp *pageant_request_keylist_1(void)
|
|
{
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH1_AGENTC_REQUEST_RSA_IDENTITIES);
|
|
if (pageant_client_op_query(pco) == SSH1_AGENT_RSA_IDENTITIES_ANSWER)
|
|
return pco;
|
|
pageant_client_op_free(pco);
|
|
return NULL;
|
|
}
|
|
|
|
static PageantClientOp *pageant_request_keylist_2(void)
|
|
{
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH2_AGENTC_REQUEST_IDENTITIES);
|
|
if (pageant_client_op_query(pco) == SSH2_AGENT_IDENTITIES_ANSWER)
|
|
return pco;
|
|
pageant_client_op_free(pco);
|
|
return NULL;
|
|
}
|
|
|
|
static PageantClientOp *pageant_request_keylist_extended(void)
|
|
{
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH2_AGENTC_EXTENSION);
|
|
put_stringpl(pco, extension_names[EXT_LIST_EXTENDED]);
|
|
if (pageant_client_op_query(pco) == SSH_AGENT_SUCCESS)
|
|
return pco;
|
|
pageant_client_op_free(pco);
|
|
return NULL;
|
|
}
|
|
|
|
static KeyList *pageant_get_keylist(unsigned ssh_version)
|
|
{
|
|
PageantClientOp *pco;
|
|
bool list_is_extended = false;
|
|
|
|
if (ssh_version == 1) {
|
|
pco = pageant_request_keylist_1();
|
|
} else {
|
|
if ((pco = pageant_request_keylist_extended()) != NULL)
|
|
list_is_extended = true;
|
|
else
|
|
pco = pageant_request_keylist_2();
|
|
}
|
|
|
|
if (!pco)
|
|
return NULL;
|
|
|
|
KeyList *kl = snew(KeyList);
|
|
kl->nkeys = get_uint32(pco);
|
|
kl->keys = snewn(kl->nkeys, struct KeyListEntry);
|
|
kl->broken = false;
|
|
|
|
for (size_t i = 0; i < kl->nkeys && !get_err(pco); i++) {
|
|
if (ssh_version == 1) {
|
|
int bloblen = rsa_ssh1_public_blob_len(
|
|
make_ptrlen(get_ptr(pco), get_avail(pco)));
|
|
if (bloblen < 0) {
|
|
kl->broken = true;
|
|
bloblen = 0;
|
|
}
|
|
kl->keys[i].blob = get_data(pco, bloblen);
|
|
} else {
|
|
kl->keys[i].blob = get_string(pco);
|
|
}
|
|
kl->keys[i].comment = get_string(pco);
|
|
|
|
if (list_is_extended) {
|
|
ptrlen key_ext_info = get_string(pco);
|
|
BinarySource src[1];
|
|
BinarySource_BARE_INIT_PL(src, key_ext_info);
|
|
|
|
kl->keys[i].flags = get_uint32(src);
|
|
} else {
|
|
kl->keys[i].flags = 0;
|
|
}
|
|
}
|
|
|
|
if (get_err(pco))
|
|
kl->broken = true;
|
|
kl->raw_data = pco->buf;
|
|
pco->buf = NULL;
|
|
pageant_client_op_free(pco);
|
|
return kl;
|
|
}
|
|
|
|
int pageant_add_keyfile(Filename *filename, const char *passphrase,
|
|
char **retstr, bool add_encrypted)
|
|
{
|
|
RSAKey *rkey = NULL;
|
|
ssh2_userkey *skey = NULL;
|
|
bool needs_pass;
|
|
int ret;
|
|
int attempts;
|
|
char *comment;
|
|
const char *this_passphrase;
|
|
const char *error = NULL;
|
|
int type;
|
|
|
|
if (!passphrases) {
|
|
passphrases = newtree234(NULL);
|
|
}
|
|
|
|
*retstr = NULL;
|
|
|
|
type = key_type(filename);
|
|
if (type != SSH_KEYTYPE_SSH1 && type != SSH_KEYTYPE_SSH2) {
|
|
*retstr = dupprintf("Couldn't load this key (%s)",
|
|
key_type_to_str(type));
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
|
|
if (add_encrypted && type == SSH_KEYTYPE_SSH1) {
|
|
*retstr = dupprintf("Can't add SSH-1 keys in encrypted form");
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
|
|
/*
|
|
* See if the key is already loaded (in the primary Pageant,
|
|
* which may or may not be us).
|
|
*/
|
|
{
|
|
strbuf *blob = strbuf_new();
|
|
KeyList *kl;
|
|
|
|
if (type == SSH_KEYTYPE_SSH1) {
|
|
if (!rsa1_loadpub_f(filename, BinarySink_UPCAST(blob),
|
|
NULL, &error)) {
|
|
*retstr = dupprintf("Couldn't load private key (%s)", error);
|
|
strbuf_free(blob);
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
kl = pageant_get_keylist(1);
|
|
} else {
|
|
if (!ppk_loadpub_f(filename, NULL, BinarySink_UPCAST(blob),
|
|
NULL, &error)) {
|
|
*retstr = dupprintf("Couldn't load private key (%s)", error);
|
|
strbuf_free(blob);
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
kl = pageant_get_keylist(2);
|
|
}
|
|
|
|
if (kl) {
|
|
if (kl->broken) {
|
|
*retstr = dupstr("Received broken key list from agent");
|
|
keylist_free(kl);
|
|
strbuf_free(blob);
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
|
|
for (size_t i = 0; i < kl->nkeys; i++) {
|
|
/*
|
|
* If the key already exists in the agent, we're done,
|
|
* except in the following special cases:
|
|
*
|
|
* It's encrypted in the agent, and we're being asked
|
|
* to add it unencrypted, in which case we still want
|
|
* to upload the unencrypted version to cause the key
|
|
* to become decrypted.
|
|
* (Rationale: if you know in advance you're going to
|
|
* want it, and don't want to be interrupted at an
|
|
* unpredictable moment to be asked for the
|
|
* passphrase.)
|
|
*
|
|
* The agent only has cleartext, and we're being asked
|
|
* to add it encrypted, in which case we'll add the
|
|
* encrypted form.
|
|
* (Rationale: if you might want to re-encrypt the key
|
|
* at some future point, but it happened to have been
|
|
* initially added in cleartext, perhaps by something
|
|
* other than Pageant.)
|
|
*/
|
|
if (ptrlen_eq_ptrlen(ptrlen_from_strbuf(blob),
|
|
kl->keys[i].blob)) {
|
|
bool have_unencrypted =
|
|
!(kl->keys[i].flags &
|
|
LIST_EXTENDED_FLAG_HAS_NO_CLEARTEXT_KEY);
|
|
bool have_encrypted =
|
|
(kl->keys[i].flags &
|
|
LIST_EXTENDED_FLAG_HAS_ENCRYPTED_KEY_FILE);
|
|
if ((have_unencrypted && !add_encrypted)
|
|
|| (have_encrypted && add_encrypted)) {
|
|
/* Key is already present in the desired form;
|
|
* we can now leave. */
|
|
keylist_free(kl);
|
|
strbuf_free(blob);
|
|
return PAGEANT_ACTION_OK;
|
|
}
|
|
}
|
|
}
|
|
|
|
keylist_free(kl);
|
|
}
|
|
|
|
strbuf_free(blob);
|
|
}
|
|
|
|
if (add_encrypted) {
|
|
const char *load_error;
|
|
LoadedFile *lf = lf_load_keyfile(filename, &load_error);
|
|
if (!lf) {
|
|
*retstr = dupstr(load_error);
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH2_AGENTC_EXTENSION);
|
|
put_stringpl(pco, extension_names[EXT_ADD_PPK]);
|
|
put_string(pco, lf->data, lf->len);
|
|
|
|
lf_free(lf);
|
|
|
|
unsigned reply = pageant_client_op_query(pco);
|
|
pageant_client_op_free(pco);
|
|
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
if (reply == SSH_AGENT_FAILURE) {
|
|
/* The agent didn't understand the protocol extension
|
|
* at all. */
|
|
*retstr = dupstr("Agent doesn't support adding "
|
|
"encrypted keys");
|
|
} else {
|
|
*retstr = dupstr("The already running agent "
|
|
"refused to add the key.");
|
|
}
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
|
|
return PAGEANT_ACTION_OK;
|
|
}
|
|
|
|
error = NULL;
|
|
if (type == SSH_KEYTYPE_SSH1)
|
|
needs_pass = rsa1_encrypted_f(filename, &comment);
|
|
else
|
|
needs_pass = ppk_encrypted_f(filename, &comment);
|
|
attempts = 0;
|
|
if (type == SSH_KEYTYPE_SSH1)
|
|
rkey = snew(RSAKey);
|
|
|
|
/*
|
|
* Loop round repeatedly trying to load the key, until we either
|
|
* succeed, fail for some serious reason, or run out of
|
|
* passphrases to try.
|
|
*/
|
|
while (1) {
|
|
if (needs_pass) {
|
|
|
|
/*
|
|
* If we've been given a passphrase on input, try using
|
|
* it. Otherwise, try one from our tree234 of previously
|
|
* useful passphrases.
|
|
*/
|
|
if (passphrase) {
|
|
this_passphrase = (attempts == 0 ? passphrase : NULL);
|
|
} else {
|
|
this_passphrase = (const char *)index234(passphrases, attempts);
|
|
}
|
|
|
|
if (!this_passphrase) {
|
|
/*
|
|
* Run out of passphrases to try.
|
|
*/
|
|
*retstr = comment;
|
|
sfree(rkey);
|
|
return PAGEANT_ACTION_NEED_PP;
|
|
}
|
|
} else
|
|
this_passphrase = "";
|
|
|
|
if (type == SSH_KEYTYPE_SSH1)
|
|
ret = rsa1_load_f(filename, rkey, this_passphrase, &error);
|
|
else {
|
|
skey = ppk_load_f(filename, this_passphrase, &error);
|
|
if (skey == SSH2_WRONG_PASSPHRASE)
|
|
ret = -1;
|
|
else if (!skey)
|
|
ret = 0;
|
|
else
|
|
ret = 1;
|
|
}
|
|
|
|
if (ret == 0) {
|
|
/*
|
|
* Failed to load the key file, for some reason other than
|
|
* a bad passphrase.
|
|
*/
|
|
*retstr = dupstr(error);
|
|
sfree(rkey);
|
|
if (comment)
|
|
sfree(comment);
|
|
return PAGEANT_ACTION_FAILURE;
|
|
} else if (ret == 1) {
|
|
/*
|
|
* Successfully loaded the key file.
|
|
*/
|
|
break;
|
|
} else {
|
|
/*
|
|
* Passphrase wasn't right; go round again.
|
|
*/
|
|
attempts++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If we get here, we've successfully loaded the key into
|
|
* rkey/skey, but not yet added it to the agent.
|
|
*/
|
|
|
|
/*
|
|
* If the key was successfully decrypted, save the passphrase for
|
|
* use with other keys we try to load.
|
|
*/
|
|
{
|
|
char *pp_copy = dupstr(this_passphrase);
|
|
if (addpos234(passphrases, pp_copy, 0) != pp_copy) {
|
|
/* No need; it was already there. */
|
|
smemclr(pp_copy, strlen(pp_copy));
|
|
sfree(pp_copy);
|
|
}
|
|
}
|
|
|
|
if (comment)
|
|
sfree(comment);
|
|
|
|
if (type == SSH_KEYTYPE_SSH1) {
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH1_AGENTC_ADD_RSA_IDENTITY);
|
|
rsa_ssh1_private_blob_agent(BinarySink_UPCAST(pco), rkey);
|
|
put_stringz(pco, rkey->comment);
|
|
unsigned reply = pageant_client_op_query(pco);
|
|
pageant_client_op_free(pco);
|
|
|
|
freersakey(rkey);
|
|
sfree(rkey);
|
|
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
*retstr = dupstr("The already running agent "
|
|
"refused to add the key.");
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
} else {
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH2_AGENTC_ADD_IDENTITY);
|
|
put_stringz(pco, ssh_key_ssh_id(skey->key));
|
|
ssh_key_openssh_blob(skey->key, BinarySink_UPCAST(pco));
|
|
put_stringz(pco, skey->comment);
|
|
unsigned reply = pageant_client_op_query(pco);
|
|
pageant_client_op_free(pco);
|
|
|
|
sfree(skey->comment);
|
|
ssh_key_free(skey->key);
|
|
sfree(skey);
|
|
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
*retstr = dupstr("The already running agent "
|
|
"refused to add the key.");
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
}
|
|
return PAGEANT_ACTION_OK;
|
|
}
|
|
|
|
int pageant_enum_keys(pageant_key_enum_fn_t callback, void *callback_ctx,
|
|
char **retstr)
|
|
{
|
|
KeyList *kl1 = NULL, *kl2 = NULL;
|
|
struct pageant_pubkey cbkey;
|
|
int toret = PAGEANT_ACTION_FAILURE;
|
|
|
|
kl1 = pageant_get_keylist(1);
|
|
if (kl1 && kl1->broken) {
|
|
*retstr = dupstr("Received broken SSH-1 key list from agent");
|
|
goto out;
|
|
}
|
|
|
|
kl2 = pageant_get_keylist(2);
|
|
if (kl2 && kl2->broken) {
|
|
*retstr = dupstr("Received broken SSH-2 key list from agent");
|
|
goto out;
|
|
}
|
|
|
|
if (kl1) {
|
|
for (size_t i = 0; i < kl1->nkeys; i++) {
|
|
cbkey.blob = strbuf_dup(kl1->keys[i].blob);
|
|
cbkey.comment = mkstr(kl1->keys[i].comment);
|
|
cbkey.ssh_version = 1;
|
|
|
|
/* Decode public blob into a key in order to fingerprint it */
|
|
RSAKey rkey;
|
|
memset(&rkey, 0, sizeof(rkey));
|
|
{
|
|
BinarySource src[1];
|
|
BinarySource_BARE_INIT_PL(src, kl1->keys[i].blob);
|
|
get_rsa_ssh1_pub(src, &rkey, RSA_SSH1_EXPONENT_FIRST);
|
|
if (get_err(src)) {
|
|
*retstr = dupstr(
|
|
"Received an invalid SSH-1 key from agent");
|
|
goto out;
|
|
}
|
|
}
|
|
char **fingerprints = rsa_ssh1_fake_all_fingerprints(&rkey);
|
|
freersakey(&rkey);
|
|
|
|
callback(callback_ctx, fingerprints, cbkey.comment,
|
|
kl1->keys[i].flags, &cbkey);
|
|
|
|
strbuf_free(cbkey.blob);
|
|
sfree(cbkey.comment);
|
|
ssh2_free_all_fingerprints(fingerprints);
|
|
}
|
|
}
|
|
|
|
if (kl2) {
|
|
for (size_t i = 0; i < kl2->nkeys; i++) {
|
|
cbkey.blob = strbuf_dup(kl2->keys[i].blob);
|
|
cbkey.comment = mkstr(kl2->keys[i].comment);
|
|
cbkey.ssh_version = 2;
|
|
|
|
char **fingerprints =
|
|
ssh2_all_fingerprints_for_blob(kl2->keys[i].blob);
|
|
|
|
callback(callback_ctx, fingerprints, cbkey.comment,
|
|
kl2->keys[i].flags, &cbkey);
|
|
|
|
ssh2_free_all_fingerprints(fingerprints);
|
|
sfree(cbkey.comment);
|
|
strbuf_free(cbkey.blob);
|
|
}
|
|
}
|
|
|
|
*retstr = NULL;
|
|
toret = PAGEANT_ACTION_OK;
|
|
out:
|
|
if (kl1)
|
|
keylist_free(kl1);
|
|
if (kl2)
|
|
keylist_free(kl2);
|
|
return toret;
|
|
}
|
|
|
|
int pageant_delete_key(struct pageant_pubkey *key, char **retstr)
|
|
{
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
|
|
if (key->ssh_version == 1) {
|
|
put_byte(pco, SSH1_AGENTC_REMOVE_RSA_IDENTITY);
|
|
put_data(pco, key->blob->s, key->blob->len);
|
|
} else {
|
|
put_byte(pco, SSH2_AGENTC_REMOVE_IDENTITY);
|
|
put_string(pco, key->blob->s, key->blob->len);
|
|
}
|
|
|
|
unsigned reply = pageant_client_op_query(pco);
|
|
pageant_client_op_free(pco);
|
|
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
*retstr = dupstr("Agent failed to delete key");
|
|
return PAGEANT_ACTION_FAILURE;
|
|
} else {
|
|
*retstr = NULL;
|
|
return PAGEANT_ACTION_OK;
|
|
}
|
|
}
|
|
|
|
int pageant_delete_all_keys(char **retstr)
|
|
{
|
|
PageantClientOp *pco;
|
|
unsigned reply;
|
|
|
|
pco = pageant_client_op_new();
|
|
put_byte(pco, SSH2_AGENTC_REMOVE_ALL_IDENTITIES);
|
|
reply = pageant_client_op_query(pco);
|
|
pageant_client_op_free(pco);
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
*retstr = dupstr("Agent failed to delete SSH-2 keys");
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
|
|
pco = pageant_client_op_new();
|
|
put_byte(pco, SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES);
|
|
reply = pageant_client_op_query(pco);
|
|
pageant_client_op_free(pco);
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
*retstr = dupstr("Agent failed to delete SSH-1 keys");
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
|
|
*retstr = NULL;
|
|
return PAGEANT_ACTION_OK;
|
|
}
|
|
|
|
int pageant_reencrypt_key(struct pageant_pubkey *key, char **retstr)
|
|
{
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
|
|
if (key->ssh_version == 1) {
|
|
*retstr = dupstr("Can't re-encrypt an SSH-1 key");
|
|
pageant_client_op_free(pco);
|
|
return PAGEANT_ACTION_FAILURE;
|
|
} else {
|
|
put_byte(pco, SSH2_AGENTC_EXTENSION);
|
|
put_stringpl(pco, extension_names[EXT_REENCRYPT]);
|
|
put_string(pco, key->blob->s, key->blob->len);
|
|
}
|
|
|
|
unsigned reply = pageant_client_op_query(pco);
|
|
pageant_client_op_free(pco);
|
|
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
if (reply == SSH_AGENT_FAILURE) {
|
|
/* The agent didn't understand the protocol extension at all. */
|
|
*retstr = dupstr("Agent doesn't support encrypted keys");
|
|
} else {
|
|
*retstr = dupstr("Agent failed to re-encrypt key");
|
|
}
|
|
return PAGEANT_ACTION_FAILURE;
|
|
} else {
|
|
*retstr = NULL;
|
|
return PAGEANT_ACTION_OK;
|
|
}
|
|
}
|
|
|
|
int pageant_reencrypt_all_keys(char **retstr)
|
|
{
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH2_AGENTC_EXTENSION);
|
|
put_stringpl(pco, extension_names[EXT_REENCRYPT_ALL]);
|
|
unsigned reply = pageant_client_op_query(pco);
|
|
uint32_t failures = get_uint32(pco);
|
|
pageant_client_op_free(pco);
|
|
if (reply != SSH_AGENT_SUCCESS) {
|
|
if (reply == SSH_AGENT_FAILURE) {
|
|
/* The agent didn't understand the protocol extension at all. */
|
|
*retstr = dupstr("Agent doesn't support encrypted keys");
|
|
} else {
|
|
*retstr = dupstr("Agent failed to re-encrypt any keys");
|
|
}
|
|
return PAGEANT_ACTION_FAILURE;
|
|
} else if (failures == 1) {
|
|
/* special case for English grammar */
|
|
*retstr = dupstr("1 key remains unencrypted");
|
|
return PAGEANT_ACTION_WARNING;
|
|
} else if (failures > 0) {
|
|
*retstr = dupprintf("%"PRIu32" keys remain unencrypted", failures);
|
|
return PAGEANT_ACTION_WARNING;
|
|
} else {
|
|
*retstr = NULL;
|
|
return PAGEANT_ACTION_OK;
|
|
}
|
|
}
|
|
|
|
int pageant_sign(struct pageant_pubkey *key, ptrlen message, strbuf *out,
|
|
uint32_t flags, char **retstr)
|
|
{
|
|
PageantClientOp *pco = pageant_client_op_new();
|
|
put_byte(pco, SSH2_AGENTC_SIGN_REQUEST);
|
|
put_string(pco, key->blob->s, key->blob->len);
|
|
put_stringpl(pco, message);
|
|
put_uint32(pco, flags);
|
|
unsigned reply = pageant_client_op_query(pco);
|
|
ptrlen signature = get_string(pco);
|
|
|
|
if (reply == SSH2_AGENT_SIGN_RESPONSE && !get_err(pco)) {
|
|
*retstr = NULL;
|
|
put_datapl(out, signature);
|
|
pageant_client_op_free(pco);
|
|
return PAGEANT_ACTION_OK;
|
|
} else {
|
|
*retstr = dupstr("Agent failed to create signature");
|
|
pageant_client_op_free(pco);
|
|
return PAGEANT_ACTION_FAILURE;
|
|
}
|
|
}
|
|
|
|
struct pageant_pubkey *pageant_pubkey_copy(struct pageant_pubkey *orig)
|
|
{
|
|
struct pageant_pubkey *copy = snew(struct pageant_pubkey);
|
|
copy->blob = strbuf_new();
|
|
put_data(copy->blob, orig->blob->s, orig->blob->len);
|
|
copy->comment = orig->comment ? dupstr(orig->comment) : NULL;
|
|
copy->ssh_version = orig->ssh_version;
|
|
return copy;
|
|
}
|
|
|
|
void pageant_pubkey_free(struct pageant_pubkey *key)
|
|
{
|
|
sfree(key->comment);
|
|
strbuf_free(key->blob);
|
|
sfree(key);
|
|
}
|