864 lines
27 KiB
C
864 lines
27 KiB
C
/* LibTomCrypt, modular cryptographic library -- Tom St Denis */
|
|
/* SPDX-License-Identifier: Unlicense */
|
|
#include "tomcrypt_private.h"
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wconversion"
|
|
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
|
|
|
|
/**
|
|
@file pem_ssh.c
|
|
SSH specific functionality to process PEM files, Steffen Jaeckel
|
|
|
|
The basic format of the key is described here:
|
|
https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
|
|
*/
|
|
|
|
#if defined(LTC_PEM_SSH)
|
|
|
|
/* Table as of
|
|
* https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-17
|
|
*/
|
|
const struct blockcipher_info ssh_ciphers[] =
|
|
{
|
|
{ .name = "none", .algo = "", .keylen = 0, .mode = cm_none },
|
|
{ .name = "aes128-cbc", .algo = "aes", .keylen = 128 / 8, .mode = cm_cbc },
|
|
{ .name = "aes128-ctr", .algo = "aes", .keylen = 128 / 8, .mode = cm_ctr },
|
|
{ .name = "aes192-cbc", .algo = "aes", .keylen = 192 / 8, .mode = cm_cbc },
|
|
{ .name = "aes192-ctr", .algo = "aes", .keylen = 192 / 8, .mode = cm_ctr },
|
|
{ .name = "aes256-cbc", .algo = "aes", .keylen = 256 / 8, .mode = cm_cbc },
|
|
{ .name = "aes256-ctr", .algo = "aes", .keylen = 256 / 8, .mode = cm_ctr },
|
|
{ .name = "aes128-gcm@openssh.com", .algo = "aes", .keylen = 128 / 8, .mode = cm_gcm },
|
|
{ .name = "aes256-gcm@openssh.com", .algo = "aes", .keylen = 256 / 8, .mode = cm_gcm },
|
|
{ .name = "blowfish128-cbc", .algo = "blowfish", .keylen = 128 / 8, .mode = cm_cbc },
|
|
{ .name = "blowfish128-ctr", .algo = "blowfish", .keylen = 128 / 8, .mode = cm_ctr },
|
|
/* The algo name doesn't matter, it's only used in pem-info */
|
|
{ .name = "chacha20-poly1305@openssh.com", .algo = "c20p1305", .keylen = 256 / 8, .mode = cm_stream | cm_openssh },
|
|
{ .name = "des-cbc", .algo = "des", .keylen = 64 / 8, .mode = cm_cbc },
|
|
{ .name = "3des-cbc", .algo = "3des", .keylen = 192 / 8, .mode = cm_cbc },
|
|
{ .name = "3des-ctr", .algo = "3des", .keylen = 192 / 8, .mode = cm_ctr },
|
|
{ .name = "serpent128-cbc", .algo = "serpent", .keylen = 128 / 8, .mode = cm_cbc },
|
|
{ .name = "serpent128-ctr", .algo = "serpent", .keylen = 128 / 8, .mode = cm_ctr },
|
|
{ .name = "serpent192-cbc", .algo = "serpent", .keylen = 192 / 8, .mode = cm_cbc },
|
|
{ .name = "serpent192-ctr", .algo = "serpent", .keylen = 192 / 8, .mode = cm_ctr },
|
|
{ .name = "serpent256-cbc", .algo = "serpent", .keylen = 256 / 8, .mode = cm_cbc },
|
|
{ .name = "serpent256-ctr", .algo = "serpent", .keylen = 256 / 8, .mode = cm_ctr },
|
|
{ .name = "twofish128-cbc", .algo = "twofish", .keylen = 128 / 8, .mode = cm_cbc },
|
|
{ .name = "twofish128-ctr", .algo = "twofish", .keylen = 128 / 8, .mode = cm_ctr },
|
|
{ .name = "twofish192-cbc", .algo = "twofish", .keylen = 192 / 8, .mode = cm_cbc },
|
|
{ .name = "twofish192-ctr", .algo = "twofish", .keylen = 192 / 8, .mode = cm_ctr },
|
|
{ .name = "twofish-cbc", .algo = "twofish", .keylen = 256 / 8, .mode = cm_cbc },
|
|
{ .name = "twofish256-cbc", .algo = "twofish", .keylen = 256 / 8, .mode = cm_cbc },
|
|
{ .name = "twofish256-ctr", .algo = "twofish", .keylen = 256 / 8, .mode = cm_ctr },
|
|
};
|
|
const unsigned long ssh_ciphers_num = LTC_ARRAY_SIZE(ssh_ciphers);
|
|
|
|
struct kdf_options {
|
|
const char *name;
|
|
const struct blockcipher_info *cipher;
|
|
unsigned char salt[64];
|
|
unsigned long saltlen;
|
|
ulong32 num_rounds;
|
|
struct password pw;
|
|
};
|
|
|
|
#ifdef LTC_MECC
|
|
static int s_ssh_find_ecc(const char *pka, const ltc_ecc_curve **curve)
|
|
{
|
|
int err;
|
|
const char* prefix = "ecdsa-sha2-";
|
|
unsigned long prefixlen = XSTRLEN(prefix);
|
|
if (strstr(pka, prefix) == NULL) return CRYPT_PK_INVALID_TYPE;
|
|
if ((err = ecc_find_curve(pka + prefixlen, curve)) != CRYPT_OK) return err;
|
|
return CRYPT_OK;
|
|
}
|
|
|
|
static int s_ssh_find_init_ecc(const char *pka, ltc_pka_key *key)
|
|
{
|
|
int err;
|
|
const ltc_ecc_curve *cu;
|
|
if ((err = s_ssh_find_ecc(pka, &cu)) != CRYPT_OK) return err;
|
|
return ecc_set_curve(cu, &key->u.ecc);
|
|
}
|
|
|
|
static int s_ssh_decode_ecdsa(const unsigned char *in, unsigned long *inlen, ltc_pka_key *pka_key, enum pem_flags type)
|
|
{
|
|
int err;
|
|
unsigned char groupname[64], buf0[512], buf1[512];
|
|
unsigned long groupnamelen = sizeof(groupname), buf0len = sizeof(buf0), buf1len = sizeof(buf1);
|
|
unsigned long remaining, cur_len, keylen;
|
|
const unsigned char *p, *key;
|
|
|
|
p = in;
|
|
cur_len = *inlen;
|
|
remaining = *inlen;
|
|
|
|
err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_STRING, groupname, &groupnamelen,
|
|
LTC_SSHDATA_STRING, buf0, &buf0len,
|
|
LTC_SSHDATA_STRING, buf1, &buf1len,
|
|
LTC_SSHDATA_EOL, LTC_NULL);
|
|
if (err == CRYPT_OK) {
|
|
key = buf1;
|
|
keylen = buf1len;
|
|
} else if (err == CRYPT_BUFFER_OVERFLOW && buf0len != sizeof(buf0) && buf1len == sizeof(buf1)) {
|
|
key = buf0;
|
|
keylen = buf0len;
|
|
} else {
|
|
goto cleanup;
|
|
}
|
|
|
|
remaining -= cur_len;
|
|
cur_len = remaining;
|
|
|
|
err = ecc_set_key(key, keylen, type == pf_public ? PK_PUBLIC : PK_PRIVATE, &pka_key->u.ecc);
|
|
|
|
cleanup:
|
|
zeromem(groupname, sizeof(groupname));
|
|
zeromem(buf0, sizeof(buf0));
|
|
zeromem(buf1, sizeof(buf1));
|
|
if (err == CRYPT_OK) {
|
|
pka_key->id = LTC_PKA_EC;
|
|
*inlen -= remaining;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
#endif
|
|
|
|
#ifdef LTC_CURVE25519
|
|
static int s_ssh_decode_ed25519(const unsigned char *in, unsigned long *inlen, ltc_pka_key *key, enum pem_flags type)
|
|
{
|
|
int err;
|
|
unsigned char pubkey[64], privkey[96];
|
|
unsigned long pubkeylen = sizeof(pubkey), privkeylen = sizeof(privkey);
|
|
unsigned long remaining, cur_len;
|
|
const unsigned char *p;
|
|
|
|
p = in;
|
|
cur_len = *inlen;
|
|
remaining = *inlen;
|
|
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_STRING, pubkey, &pubkeylen,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
|
|
if (type == pf_public) {
|
|
if ((err = ed25519_import_raw(pubkey, pubkeylen, PK_PUBLIC, &key->u.ed25519)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
key->id = LTC_PKA_ED25519;
|
|
goto cleanup;
|
|
}
|
|
|
|
p += cur_len;
|
|
remaining -= cur_len;
|
|
cur_len = remaining;
|
|
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_STRING, privkey, &privkeylen,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
if ((err = ed25519_import_raw(privkey, privkeylen, PK_PRIVATE, &key->u.ed25519)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
|
|
key->id = LTC_PKA_ED25519;
|
|
|
|
cleanup:
|
|
zeromem(pubkey, sizeof(pubkey));
|
|
zeromem(privkey, sizeof(privkey));
|
|
if (err == CRYPT_OK) {
|
|
remaining -= cur_len;
|
|
*inlen -= remaining;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
#endif
|
|
|
|
#ifdef LTC_MRSA
|
|
static int s_ssh_decode_dsa(const unsigned char *in, unsigned long *inlen, ltc_pka_key *key, enum pem_flags type)
|
|
{
|
|
int err, stat;
|
|
unsigned long remaining, cur_len;
|
|
const unsigned char *p;
|
|
if ((err = dsa_int_init(&key->u.dsa)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
|
|
p = in;
|
|
cur_len = *inlen;
|
|
remaining = *inlen;
|
|
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_MPINT, key->u.dsa.p,
|
|
LTC_SSHDATA_MPINT, key->u.dsa.q,
|
|
LTC_SSHDATA_MPINT, key->u.dsa.g,
|
|
LTC_SSHDATA_MPINT, key->u.dsa.y,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
key->u.dsa.qord = ltc_mp_unsigned_bin_size(key->u.dsa.q);
|
|
if ((err = dsa_int_validate_pqg(&key->u.dsa, &stat)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
if (stat == 0) {
|
|
err = CRYPT_INVALID_PACKET;
|
|
goto cleanup;
|
|
}
|
|
|
|
if (type == pf_public) {
|
|
key->id = LTC_PKA_DSA;
|
|
key->u.dsa.type = PK_PUBLIC;
|
|
goto cleanup;
|
|
}
|
|
|
|
p += cur_len;
|
|
remaining -= cur_len;
|
|
cur_len = remaining;
|
|
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_MPINT, key->u.dsa.x,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
|
|
key->id = LTC_PKA_DSA;
|
|
key->u.dsa.type = PK_PRIVATE;
|
|
|
|
cleanup:
|
|
if (err != CRYPT_OK) {
|
|
dsa_free(&key->u.dsa);
|
|
} else {
|
|
remaining -= cur_len;
|
|
*inlen -= remaining;
|
|
}
|
|
|
|
return err;
|
|
|
|
}
|
|
#endif
|
|
|
|
#ifdef LTC_MRSA
|
|
static int s_ssh_decode_rsa(const unsigned char *in, unsigned long *inlen, ltc_pka_key *key, enum pem_flags type)
|
|
{
|
|
int err;
|
|
void *tmp1, *tmp2;
|
|
unsigned long remaining, cur_len;
|
|
const unsigned char *p;
|
|
|
|
if ((err = rsa_init(&key->u.rsa)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
|
|
p = in;
|
|
cur_len = *inlen;
|
|
remaining = *inlen;
|
|
|
|
/* ssh-rsa public and private keys contain `e` and `N` in a different order
|
|
* public contains `e`, then `N`
|
|
* private contains `N`, then `e`
|
|
* change the order later on if we import a public key */
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_MPINT, key->u.rsa.N,
|
|
LTC_SSHDATA_MPINT, key->u.rsa.e,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
|
|
p += cur_len;
|
|
remaining -= cur_len;
|
|
cur_len = remaining;
|
|
|
|
if (type == pf_public) {
|
|
/* c.f. comment above */
|
|
void *exch = key->u.rsa.N;
|
|
key->u.rsa.N = key->u.rsa.e;
|
|
key->u.rsa.e = exch;
|
|
key->id = LTC_PKA_RSA;
|
|
key->u.rsa.type = PK_PUBLIC;
|
|
*inlen -= remaining;
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((err = ltc_mp_init_multi(&tmp1, &tmp2, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_MPINT, key->u.rsa.d,
|
|
LTC_SSHDATA_MPINT, key->u.rsa.qP,
|
|
LTC_SSHDATA_MPINT, key->u.rsa.p,
|
|
LTC_SSHDATA_MPINT, key->u.rsa.q,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup_tmps;
|
|
}
|
|
|
|
if ((err = ltc_mp_sub_d(key->u.rsa.p, 1, tmp1)) != CRYPT_OK) { goto cleanup_tmps; } /* tmp1 = q-1 */
|
|
if ((err = ltc_mp_sub_d(key->u.rsa.q, 1, tmp2)) != CRYPT_OK) { goto cleanup_tmps; } /* tmp2 = p-1 */
|
|
if ((err = ltc_mp_mod(key->u.rsa.d, tmp1, key->u.rsa.dP)) != CRYPT_OK) { goto cleanup_tmps; } /* dP = d mod p-1 */
|
|
if ((err = ltc_mp_mod(key->u.rsa.d, tmp2, key->u.rsa.dQ)) != CRYPT_OK) { goto cleanup_tmps; } /* dQ = d mod q-1 */
|
|
|
|
key->id = LTC_PKA_RSA;
|
|
key->u.rsa.type = PK_PRIVATE;
|
|
|
|
cleanup_tmps:
|
|
ltc_mp_deinit_multi(tmp2, tmp1, LTC_NULL);
|
|
cleanup:
|
|
if (err != CRYPT_OK) {
|
|
rsa_free(&key->u.rsa);
|
|
} else {
|
|
remaining -= cur_len;
|
|
*inlen -= remaining;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
#endif
|
|
|
|
struct ssh_pka {
|
|
struct str name;
|
|
enum ltc_pka_id id;
|
|
int (*find)(const char*, const ltc_ecc_curve **);
|
|
int (*init)(const char*, ltc_pka_key*);
|
|
int (*decode)(const unsigned char*, unsigned long*, ltc_pka_key*, enum pem_flags);
|
|
};
|
|
|
|
struct ssh_pka ssh_pkas[] = {
|
|
#ifdef LTC_CURVE25519
|
|
{ SET_CSTR(.name, "ssh-ed25519"),
|
|
LTC_PKA_ED25519,
|
|
NULL,
|
|
NULL,
|
|
s_ssh_decode_ed25519 },
|
|
#endif
|
|
#ifdef LTC_MRSA
|
|
{ SET_CSTR(.name, "ssh-rsa"),
|
|
LTC_PKA_RSA,
|
|
NULL,
|
|
NULL,
|
|
s_ssh_decode_rsa },
|
|
#endif
|
|
#ifdef LTC_MDSA
|
|
{ SET_CSTR(.name, "ssh-dss"),
|
|
LTC_PKA_DSA,
|
|
NULL,
|
|
NULL,
|
|
s_ssh_decode_dsa },
|
|
#endif
|
|
#ifdef LTC_MECC
|
|
{ { NULL, 0 },
|
|
LTC_PKA_EC,
|
|
s_ssh_find_ecc,
|
|
s_ssh_find_init_ecc,
|
|
s_ssh_decode_ecdsa },
|
|
#endif
|
|
};
|
|
|
|
static int s_decode_key(const unsigned char *in, unsigned long *inlen, ltc_pka_key *key, char **comment, enum pem_flags type)
|
|
{
|
|
int err;
|
|
ulong32 check1, check2;
|
|
unsigned char pka[64];
|
|
unsigned long pkalen = sizeof(pka);
|
|
unsigned long remaining, cur_len;
|
|
const unsigned char *p;
|
|
unsigned long n;
|
|
|
|
LTC_ARGCHK(in != NULL);
|
|
LTC_ARGCHK(inlen != NULL);
|
|
LTC_ARGCHK(key != NULL);
|
|
|
|
p = in;
|
|
cur_len = *inlen;
|
|
remaining = *inlen;
|
|
|
|
if (type != pf_public) {
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_UINT32, &check1,
|
|
LTC_SSHDATA_UINT32, &check2,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
if (check1 != check2) {
|
|
return CRYPT_INVALID_PACKET;
|
|
}
|
|
|
|
p += cur_len;
|
|
remaining -= cur_len;
|
|
cur_len = remaining;
|
|
}
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_STRING, pka, &pkalen,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
|
|
p += cur_len;
|
|
remaining -= cur_len;
|
|
cur_len = remaining;
|
|
|
|
for (n = 0; n < LTC_ARRAY_SIZE(ssh_pkas); ++n) {
|
|
if (ssh_pkas[n].name.p != NULL) {
|
|
if (pkalen != ssh_pkas[n].name.len
|
|
|| XMEMCMP(pka, ssh_pkas[n].name.p, ssh_pkas[n].name.len) != 0) continue;
|
|
} else {
|
|
if ((ssh_pkas[n].init == NULL) ||
|
|
(ssh_pkas[n].init((char*)pka, key) != CRYPT_OK)) continue;
|
|
}
|
|
if ((err = ssh_pkas[n].decode(p, &cur_len, key, type)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
break;
|
|
}
|
|
if (n == LTC_ARRAY_SIZE(ssh_pkas)) {
|
|
return CRYPT_PK_INVALID_TYPE;
|
|
}
|
|
|
|
p += cur_len;
|
|
remaining -= cur_len;
|
|
cur_len = remaining;
|
|
|
|
if (cur_len != 0 && comment) {
|
|
unsigned long commentlen = cur_len;
|
|
char *c = XMALLOC(commentlen);
|
|
if (c == NULL) {
|
|
return CRYPT_MEM;
|
|
}
|
|
if ((err = ssh_decode_sequence_multi(p, &cur_len,
|
|
LTC_SSHDATA_STRING, c, &commentlen,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
if (commentlen == 0) {
|
|
XFREE(c);
|
|
} else {
|
|
*comment = c;
|
|
}
|
|
}
|
|
|
|
p += cur_len;
|
|
remaining -= cur_len;
|
|
|
|
return remaining ? padding_depad(p, &remaining, LTC_PAD_SSH) : CRYPT_OK;
|
|
}
|
|
|
|
static LTC_INLINE void skip_spaces(char **r, unsigned long *l)
|
|
{
|
|
while(*l && (**r == ' ' || **r == '\t')) {
|
|
(*r)++;
|
|
(*l)--;
|
|
}
|
|
}
|
|
|
|
static LTC_INLINE void skip_chars(char **r, unsigned long *l)
|
|
{
|
|
while(*l && (**r != ' ' && **r != '\t')) {
|
|
(*l)--;
|
|
if (**r == '\n' || **r == '\r') {
|
|
*l = 0;
|
|
} else {
|
|
(*r)++;
|
|
}
|
|
}
|
|
}
|
|
|
|
static LTC_INLINE void skip_to_eol(char **r, unsigned long *l)
|
|
{
|
|
while(*l && (**r != '\n' && **r != '\r')) {
|
|
(*l)--;
|
|
(*r)++;
|
|
}
|
|
}
|
|
|
|
static int s_parse_line(char *line, unsigned long *len, ltc_pka_key *key, char **comment)
|
|
{
|
|
int err;
|
|
unsigned long n, rlen, olen;
|
|
enum authorized_keys_elements {
|
|
ake_algo_name = 0,
|
|
ake_b64_encoded_key = 1,
|
|
ake_comment = 2
|
|
};
|
|
struct str elements[3] = { 0 };
|
|
char *r = line;
|
|
unsigned char *buf = NULL;
|
|
|
|
rlen = *len;
|
|
/* Chop up string into the three authorized_keys_elements */
|
|
for (n = 0; n < LTC_ARRAY_SIZE(elements) && rlen; ++n) {
|
|
skip_spaces(&r, &rlen);
|
|
elements[n].p = r;
|
|
if (n != 2)
|
|
skip_chars(&r, &rlen);
|
|
else
|
|
skip_to_eol(&r, &rlen);
|
|
elements[n].len = r - elements[n].p;
|
|
*r = '\0';
|
|
r++;
|
|
}
|
|
|
|
for (n = 0; n < LTC_ARRAY_SIZE(ssh_pkas); ++n) {
|
|
if (ssh_pkas[n].name.p != NULL) {
|
|
if (elements[ake_algo_name].len != ssh_pkas[n].name.len
|
|
|| XMEMCMP(elements[ake_algo_name].p, ssh_pkas[n].name.p, ssh_pkas[n].name.len) != 0) continue;
|
|
} else {
|
|
if ((ssh_pkas[n].find == NULL) ||
|
|
(ssh_pkas[n].find(elements[ake_algo_name].p, NULL) != CRYPT_OK)) continue;
|
|
}
|
|
olen = elements[ake_b64_encoded_key].len;
|
|
buf = XMALLOC(olen);
|
|
if (buf == NULL) {
|
|
return CRYPT_MEM;
|
|
}
|
|
if ((err = base64_strict_decode(elements[ake_b64_encoded_key].p, elements[ake_b64_encoded_key].len, buf, &olen)) == CRYPT_OK) {
|
|
err = s_decode_key(buf, &olen, key, comment, pf_public);
|
|
if (err == CRYPT_OK && key->id != ssh_pkas[n].id) {
|
|
err = CRYPT_PK_INVALID_TYPE;
|
|
}
|
|
}
|
|
XFREE(buf);
|
|
|
|
if (err == CRYPT_OK) {
|
|
/* Only use the comment that was maybe in the text we just processed, in case when
|
|
* there was no comment inside the SSH key.
|
|
*/
|
|
if (*comment == NULL && elements[ake_comment].p) {
|
|
*comment = XMALLOC(elements[ake_comment].len + 1);
|
|
if (*comment == NULL) {
|
|
return CRYPT_MEM;
|
|
}
|
|
XMEMCPY(*comment, elements[ake_comment].p, elements[ake_comment].len);
|
|
(*comment)[elements[ake_comment].len] = '\0';
|
|
}
|
|
*len = r - line;
|
|
return CRYPT_OK;
|
|
}
|
|
}
|
|
return CRYPT_PK_INVALID_TYPE;
|
|
}
|
|
|
|
static int s_read_authorized_keys(const void *buf, unsigned long len, ssh_authorized_key_cb cb, void *ctx)
|
|
{
|
|
char *s;
|
|
int err;
|
|
unsigned long clen = len;
|
|
ltc_pka_key *key = XCALLOC(1, sizeof(*key));
|
|
char *comment = NULL;
|
|
void *cpy = XMALLOC(len);
|
|
if (key == NULL || cpy == NULL) {
|
|
if (cpy)
|
|
XFREE(cpy);
|
|
if (key)
|
|
XFREE(key);
|
|
return CRYPT_MEM;
|
|
}
|
|
XMEMCPY(cpy, buf, len);
|
|
s = cpy;
|
|
while (clen && (err = s_parse_line(s, &clen, key, &comment)) == CRYPT_OK) {
|
|
if (cb(key, comment, ctx)) {
|
|
break;
|
|
}
|
|
s += clen;
|
|
len -= clen;
|
|
clen = len;
|
|
key = XCALLOC(1, sizeof(*key));
|
|
if (key == NULL) {
|
|
err = CRYPT_MEM;
|
|
break;
|
|
}
|
|
if (comment) {
|
|
XFREE(comment);
|
|
comment = NULL;
|
|
}
|
|
}
|
|
if (comment)
|
|
XFREE(comment);
|
|
if (cpy)
|
|
XFREE(cpy);
|
|
if (key)
|
|
XFREE(key);
|
|
return err;
|
|
}
|
|
|
|
static int s_decrypt_private_keys(unsigned char *in, unsigned long *inlen,
|
|
unsigned char *tag, unsigned long taglen,
|
|
struct kdf_options *opts)
|
|
{
|
|
int err, cipher;
|
|
unsigned long symkey_len, iv_len;
|
|
unsigned char symkey[MAXBLOCKSIZE], *iv, iv_[8] = { 0 };
|
|
enum cipher_mode mode = opts->cipher->mode & cm_modes;
|
|
|
|
LTC_ARGCHK(in != NULL);
|
|
LTC_ARGCHK(inlen != NULL);
|
|
LTC_ARGCHK(opts != NULL);
|
|
|
|
if (mode != cm_stream) {
|
|
cipher = find_cipher(opts->cipher->algo);
|
|
if (cipher == -1) {
|
|
return CRYPT_INVALID_CIPHER;
|
|
}
|
|
|
|
iv = symkey + opts->cipher->keylen;
|
|
iv_len = mode == cm_gcm ? 12 : cipher_descriptor[cipher].block_length;
|
|
symkey_len = opts->cipher->keylen + iv_len;
|
|
} else {
|
|
iv = iv_;
|
|
iv_len = sizeof(iv_);
|
|
symkey_len = 64;
|
|
}
|
|
|
|
if (sizeof(symkey) < symkey_len) {
|
|
return CRYPT_OVERFLOW;
|
|
}
|
|
|
|
if ((err = bcrypt_pbkdf_openbsd(opts->pw.pw, opts->pw.l, opts->salt, opts->saltlen,
|
|
opts->num_rounds, find_hash("sha512"), symkey, &symkey_len)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
err = pem_decrypt(in, inlen,
|
|
symkey, opts->cipher->keylen,
|
|
iv, iv_len,
|
|
tag, taglen,
|
|
opts->cipher, LTC_PAD_SSH);
|
|
zeromem(symkey, sizeof(symkey));
|
|
|
|
return err;
|
|
}
|
|
|
|
static int s_decode_header(unsigned char *in, unsigned long *inlen, struct kdf_options *opts)
|
|
{
|
|
int err;
|
|
unsigned char ciphername[64], kdfname[64], kdfoptions[128], pubkey1[2048];
|
|
unsigned long ciphernamelen = sizeof(ciphername), kdfnamelen = sizeof(kdfname);
|
|
unsigned long kdfoptionslen = sizeof(kdfoptions), pubkey1len = sizeof(pubkey1);
|
|
ulong32 num_keys;
|
|
unsigned long i;
|
|
|
|
void *magic = strstr((const char*)in, "openssh-key-v1");
|
|
unsigned long slen = XSTRLEN("openssh-key-v1");
|
|
unsigned char *start = &in[slen + 1];
|
|
unsigned long len = *inlen - slen - 1;
|
|
|
|
if (magic == NULL || magic != in) {
|
|
return CRYPT_INVALID_PACKET;
|
|
}
|
|
|
|
if ((err = ssh_decode_sequence_multi(start, &len,
|
|
LTC_SSHDATA_STRING, ciphername, &ciphernamelen,
|
|
LTC_SSHDATA_STRING, kdfname, &kdfnamelen,
|
|
LTC_SSHDATA_STRING, kdfoptions, &kdfoptionslen,
|
|
LTC_SSHDATA_UINT32, &num_keys,
|
|
LTC_SSHDATA_STRING, pubkey1, &pubkey1len,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
if (num_keys != 1) {
|
|
return CRYPT_INVALID_PACKET;
|
|
}
|
|
|
|
*inlen = len + slen + 1;
|
|
|
|
for (i = 0; i < ssh_ciphers_num; ++i) {
|
|
if (XSTRCMP((char*)ciphername, ssh_ciphers[i].name) == 0) {
|
|
opts->cipher = &ssh_ciphers[i];
|
|
break;
|
|
}
|
|
}
|
|
if (opts->cipher == NULL) {
|
|
return CRYPT_INVALID_CIPHER;
|
|
}
|
|
|
|
if (XSTRCMP((char*)kdfname, "none") == 0) {
|
|
/* NOP */
|
|
opts->name = "none";
|
|
} else if (XSTRCMP((char*)kdfname, "bcrypt") == 0) {
|
|
opts->name = "bcrypt";
|
|
opts->saltlen = sizeof(opts->salt);
|
|
len = kdfoptionslen;
|
|
if ((err = ssh_decode_sequence_multi(kdfoptions, &len,
|
|
LTC_SSHDATA_STRING, opts->salt, &opts->saltlen,
|
|
LTC_SSHDATA_UINT32, &opts->num_rounds,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
return err;
|
|
}
|
|
if (len != kdfoptionslen) {
|
|
return CRYPT_INPUT_TOO_LONG;
|
|
}
|
|
} else {
|
|
return CRYPT_INVALID_PACKET;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
|
|
static const struct pem_header_id pem_openssh[] = {
|
|
{
|
|
SET_CSTR(.start, "-----BEGIN OPENSSH PRIVATE KEY-----"),
|
|
SET_CSTR(.end, "-----END OPENSSH PRIVATE KEY-----"),
|
|
.has_more_headers = no
|
|
},
|
|
{
|
|
SET_CSTR(.start, "---- BEGIN SSH2 PUBLIC KEY ----"),
|
|
SET_CSTR(.end, "---- END SSH2 PUBLIC KEY ----"),
|
|
.has_more_headers = maybe,
|
|
.flags = pf_public
|
|
},
|
|
};
|
|
static const unsigned long pem_openssh_num = LTC_ARRAY_SIZE(pem_openssh);
|
|
|
|
static int s_decode_openssh(struct get_char *g, ltc_pka_key *k, const password_ctx *pw_ctx)
|
|
{
|
|
unsigned char *pem = NULL, *p, *privkey = NULL, *tag;
|
|
unsigned long n, w, l, privkey_len, taglen;
|
|
int err;
|
|
struct pem_headers hdr;
|
|
struct kdf_options opts = { 0 };
|
|
XMEMSET(k, 0, sizeof(*k));
|
|
w = LTC_PEM_READ_BUFSIZE * 2;
|
|
retry:
|
|
pem = XREALLOC(pem, w);
|
|
for (n = 0; n < pem_openssh_num; ++n) {
|
|
hdr.id = &pem_openssh[n];
|
|
err = pem_read(pem, &w, &hdr, g);
|
|
if (err == CRYPT_BUFFER_OVERFLOW) {
|
|
goto retry;
|
|
} else if (err == CRYPT_OK) {
|
|
break;
|
|
} else if (err != CRYPT_UNKNOWN_PEM) {
|
|
goto cleanup;
|
|
}
|
|
hdr.id = NULL;
|
|
}
|
|
/* id not found */
|
|
if (hdr.id == NULL) {
|
|
goto cleanup;
|
|
}
|
|
p = pem;
|
|
l = w;
|
|
if (hdr.id->flags != pf_public) {
|
|
if ((err = s_decode_header(pem, &w, &opts)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
p = pem + w;
|
|
l -= w;
|
|
w = l;
|
|
|
|
privkey_len = l;
|
|
privkey = XMALLOC(privkey_len);
|
|
if (privkey == NULL) {
|
|
return CRYPT_MEM;
|
|
}
|
|
|
|
if ((err = ssh_decode_sequence_multi(p, &w,
|
|
LTC_SSHDATA_STRING, privkey, &privkey_len,
|
|
LTC_SSHDATA_EOL, LTC_NULL)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
|
|
if (XSTRCMP(opts.name, "none") != 0) {
|
|
if ((pw_ctx == NULL) || (pw_ctx->callback == NULL)) {
|
|
err = CRYPT_PW_CTX_MISSING;
|
|
goto cleanup;
|
|
}
|
|
if (pw_ctx->callback(&opts.pw.pw, &opts.pw.l, pw_ctx->userdata)) {
|
|
err = CRYPT_ERROR;
|
|
goto cleanup;
|
|
}
|
|
tag = p + w;
|
|
taglen = l - w;
|
|
w = privkey_len;
|
|
if ((err = s_decrypt_private_keys(privkey, &privkey_len,
|
|
tag, taglen,
|
|
&opts)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
zeromem(opts.pw.pw, opts.pw.l);
|
|
}
|
|
|
|
p = privkey;
|
|
w = privkey_len;
|
|
}
|
|
if ((err = s_decode_key(p, &w, k, NULL, hdr.id->flags)) != CRYPT_OK) {
|
|
goto cleanup;
|
|
}
|
|
|
|
cleanup:
|
|
password_free(&opts.pw, pw_ctx);
|
|
if (privkey) {
|
|
zeromem(privkey, privkey_len);
|
|
XFREE(privkey);
|
|
}
|
|
XFREE(pem);
|
|
return err;
|
|
}
|
|
|
|
#ifndef LTC_NO_FILE
|
|
int pem_decode_openssh_filehandle(FILE *f, ltc_pka_key *k, const password_ctx *pw_ctx)
|
|
{
|
|
LTC_ARGCHK(f != NULL);
|
|
LTC_ARGCHK(k != NULL);
|
|
{
|
|
struct get_char g = { .get = pem_get_char_from_file, .data.f = f };
|
|
return s_decode_openssh(&g, k, pw_ctx);
|
|
}
|
|
}
|
|
|
|
int ssh_read_authorized_keys_filehandle(FILE *f, ssh_authorized_key_cb cb, void *ctx)
|
|
{
|
|
size_t tot_data;
|
|
void *buf;
|
|
int err;
|
|
|
|
LTC_ARGCHK(f != NULL);
|
|
LTC_ARGCHK(cb != NULL);
|
|
|
|
if (fseek(f, 0, SEEK_END) == -1)
|
|
return CRYPT_ERROR;
|
|
tot_data = ftell(f);
|
|
if (fseek(f, 0, SEEK_SET) == -1)
|
|
return CRYPT_ERROR;
|
|
buf = XMALLOC(tot_data);
|
|
if (buf == NULL) {
|
|
return CRYPT_MEM;
|
|
}
|
|
if (fread(buf, 1, tot_data, f) != tot_data) {
|
|
err = CRYPT_ERROR;
|
|
} else {
|
|
err = s_read_authorized_keys(buf, tot_data, cb, ctx);
|
|
}
|
|
XFREE(buf);
|
|
|
|
return err;
|
|
}
|
|
#endif /* LTC_NO_FILE */
|
|
|
|
int pem_decode_openssh(const void *buf, unsigned long len, ltc_pka_key *k, const password_ctx *pw_ctx)
|
|
{
|
|
LTC_ARGCHK(buf != NULL);
|
|
LTC_ARGCHK(len != 0);
|
|
LTC_ARGCHK(k != NULL);
|
|
{
|
|
struct get_char g = { .get = pem_get_char_from_buf, SET_BUFP(.data.buf, buf, len) };
|
|
return s_decode_openssh(&g, k, pw_ctx);
|
|
}
|
|
}
|
|
|
|
int ssh_read_authorized_keys(const void *buf, unsigned long len, ssh_authorized_key_cb cb, void *ctx)
|
|
{
|
|
LTC_ARGCHK(buf != NULL);
|
|
LTC_ARGCHK(len != 0);
|
|
LTC_ARGCHK(cb != NULL);
|
|
|
|
return s_read_authorized_keys(buf, len, cb, ctx);
|
|
}
|
|
#endif /* defined(LTC_PEM_SSH) */
|
|
|
|
#pragma clang diagnostic pop
|