diff --git a/Makefile b/Makefile index 969f398a3..f4dab5fa9 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,7 @@ TEST_CLI_PROGRAMS := \ TEST_PROGRAMS := \ test/test_state_coverage \ + test/onion_key \ test/test_onion BITCOIN_OBJS := \ @@ -99,8 +100,17 @@ test-cli-tests: $(TEST_CLI_PROGRAMS) cd test-cli; scripts/shutdown.sh 2>/dev/null || true set -e; cd test-cli; for args in "" --steal --unilateral --htlc-onchain; do scripts/setup.sh && scripts/test.sh $$args && scripts/shutdown.sh; done -test-onion: test/test_onion - set -e; for i in `seq 20`; do ./test/test_onion $$i; done +test-onion: test/test_onion test/onion_key + set -e; TMPF=/tmp/onion.$$$$; test/test_onion --generate $$(test/onion_key --pub `seq 20`) > $$TMPF; for k in `seq 20`; do test/test_onion --decode $$(test/onion_key --priv $$k) < $$TMPF > $$TMPF.unwrap; mv $$TMPF.unwrap $$TMPF; done; rm -f $$TMPF + +test-onion2: test/test_onion test/onion_key + set -e; TMPF=/tmp/onion.$$$$; python test/test_onion.py generate $$(test/onion_key --pub `seq 20`) > $$TMPF; for k in `seq 20`; do test/test_onion --decode $$(test/onion_key --priv $$k) < $$TMPF > $$TMPF.unwrap; mv $$TMPF.unwrap $$TMPF; done; rm -f $$TMPF + +test-onion3: test/test_onion test/onion_key + set -e; TMPF=/tmp/onion.$$$$; test/test_onion --generate $$(test/onion_key --pub `seq 20`) > $$TMPF; for k in `seq 20`; do python test/test_onion.py decode $$(test/onion_key --priv $$k) $$(test/onion_key --pub $$k) < $$TMPF > $$TMPF.unwrap; mv $$TMPF.unwrap $$TMPF; done; rm -f $$TMPF + +test-onion4: test/test_onion test/onion_key + set -e; TMPF=/tmp/onion.$$$$; python test/test_onion.py generate $$(test/onion_key --pub `seq 20`) > $$TMPF; for k in `seq 20`; do python test/test_onion.py decode $$(test/onion_key --priv $$k) $$(test/onion_key --pub $$k) < $$TMPF > $$TMPF.unwrap; mv $$TMPF.unwrap $$TMPF; done; rm -f $$TMPF check: test-cli-tests test-onion diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..4dce2dd2a --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,3 @@ +test_onion +test_state_coverage +onion_key diff --git a/test/onion_key.c b/test/onion_key.c new file mode 100644 index 000000000..06f1fe7cf --- /dev/null +++ b/test/onion_key.c @@ -0,0 +1,102 @@ +#define _GNU_SOURCE 1 +#include "secp256k1.h" +#include "secp256k1_ecdh.h" +#include "onion_key.h" +#include "version.h" +#include +#include +#include +#include +#include +#include +#include +#include + +/* Not really! */ +static void random_bytes(void *dst, size_t n) +{ + size_t i; + unsigned char *d = dst; + + for (i = 0; i < n; i++) + d[i] = random() % 256; +} + +static void random_key(secp256k1_context *ctx, + struct seckey *seckey, secp256k1_pubkey *pkey) +{ + do { + random_bytes(seckey->u.u8, sizeof(seckey->u)); + } while (!secp256k1_ec_pubkey_create(ctx, pkey, seckey->u.u8)); +} + +/* We don't want to spend a byte encoding sign, so make sure it's 0x2 */ +static void gen_keys(secp256k1_context *ctx, + struct seckey *seckey, struct compressed_pubkey *pubkey) +{ + secp256k1_pubkey pkey; + size_t len; + + random_key(ctx, seckey, &pkey); + + secp256k1_ec_pubkey_serialize(ctx, pubkey->u8, &len, &pkey, + SECP256K1_EC_COMPRESSED); + assert(len == sizeof(pubkey->u8)); +} + +void print_keypair(bool pub, bool priv) +{ + secp256k1_context *ctx; + struct seckey seckey; + struct compressed_pubkey pubkey; + char sechex[hex_str_size(sizeof(seckey))]; + char pubhex[hex_str_size(sizeof(pubkey))]; + + assert(pub || priv); + + ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + gen_keys(ctx, &seckey, &pubkey); + + hex_encode(&seckey, sizeof(seckey), sechex, sizeof(sechex)); + hex_encode(&pubkey, sizeof(pubkey), pubhex, sizeof(pubhex)); + + if (pub && priv) { + printf("%s:%s\n", sechex, pubhex); + } else { + printf("%s\n", (priv ? sechex : pubhex)); + } +} + +int main(int argc, char *argv[]) +{ + bool pub = true, priv = true; + + opt_register_noarg("--help|-h", opt_usage_and_exit, + "[...]\n" + "Generate (deterministic if seed) secp256k1 keys", + "Print this message."); + opt_register_noarg("--pub", + opt_set_invbool, &priv, + "Generate only the public key"); + opt_register_noarg("--priv", + opt_set_invbool, &pub, + "Generate only the private key"); + opt_register_version(); + + opt_parse(&argc, argv, opt_log_stderr_exit); + if (!priv && !pub) + opt_usage_exit_fail("Can't use --pub and --priv"); + + if (argc == 1) { + srandom(time(NULL) + getpid()); + print_keypair(pub, priv); + } else { + int i; + for (i = 1; i < argc; i++) { + srandom(atoi(argv[i])); + print_keypair(pub, priv); + } + } + + return 0; +} diff --git a/test/onion_key.h b/test/onion_key.h new file mode 100644 index 000000000..61cd36083 --- /dev/null +++ b/test/onion_key.h @@ -0,0 +1,24 @@ +#ifndef ONION_KEY_H +#define ONION_KEY_H +#include +#include "bitcoin/privkey.h" + +struct seckey { + union { + struct privkey k; + unsigned char u8[32]; + beint64_t be64[4]; + } u; +}; + +/* First byte is 0x02 or 0x03 indicating even or odd y */ +struct compressed_pubkey { + unsigned char u8[33]; +}; + +/* Prepend 0x02 to get pubkey for libsecp256k1 */ +struct onion_pubkey { + unsigned char u8[32]; +}; + +#endif /* ONION_KEY_H */ diff --git a/test/test_onion.c b/test/test_onion.c index 31dd96750..7a44173b9 100644 --- a/test/test_onion.c +++ b/test/test_onion.c @@ -1,6 +1,8 @@ #define _GNU_SOURCE 1 +#include "onion_key.h" #include "secp256k1.h" #include "secp256k1_ecdh.h" +#include "version.h" #include #include #include @@ -10,9 +12,14 @@ #include #include #include +#include #include #include #include +#include +#include +#include +#include /* * The client knows the server's public key S (which has corresponding @@ -27,13 +34,6 @@ of m, for each message. */ -//#define EXPORT_FRIENDLY 1 /* No crypto! */ -//#define NO_HMAC 1 /* No real hmac */ - -struct seckey { - struct sha256 k; -}; - struct enckey { struct sha256 k; }; @@ -74,28 +74,14 @@ static struct hmackey hmackey_from_secret(const unsigned char secret[32]) } -static struct iv iv_from_secret(const unsigned char secret[32], size_t i) +static void ivs_from_secret(const unsigned char secret[32], + struct iv *iv, struct iv *pad_iv) { - struct iv iv; struct sha256 sha; sha_with_seed(secret, 2, &sha); - memcpy(iv.iv, sha.u.u8, sizeof(iv.iv)); -#ifdef EXPORT_FRIENDLY - iv.iv[0] = i*2; -#endif - return iv; -} - -static struct iv pad_iv_from_secret(const unsigned char secret[32], size_t i) -{ - struct iv iv; - struct sha256 sha; - sha_with_seed(secret, 3, &sha); - memcpy(iv.iv, sha.u.u8, sizeof(iv.iv)); -#ifdef EXPORT_FRIENDLY - iv.iv[0] = i*2 + 1; -#endif - return iv; + BUILD_ASSERT(sizeof(*iv) + sizeof(*pad_iv) == sizeof(sha)); + memcpy(iv->iv, sha.u.u8, sizeof(iv->iv)); + memcpy(pad_iv->iv, sha.u.u8 + sizeof(iv->iv), sizeof(pad_iv->iv)); } /* Not really! */ @@ -108,12 +94,97 @@ static void random_bytes(void *dst, size_t n) d[i] = random() % 256; } -static void gen_keys(secp256k1_context *ctx, - struct seckey *seckey, secp256k1_pubkey *pubkey) +/* Compressed key would start with 0x3? Subtract from group. Thanks + * Greg Maxwell. */ +static void flip_key(struct seckey *seckey) +{ + int i; + bool carry = 0; + + const int64_t group[] = { + 0xFFFFFFFFFFFFFFFFULL, + 0xFFFFFFFFFFFFFFFEULL, + 0xBAAEDCE6AF48A03BULL, + 0xBFD25E8CD0364141ULL + }; + + for (i = 3; i >= 0; i--) { + uint64_t v = be64_to_cpu(seckey->u.be64[i]); + if (carry) { + /* Beware wrap if v == 0xFFFF.... */ + carry = (group[i] <= v); + v++; + } else + carry = (group[i] < v); + + v = group[i] - v; + seckey->u.be64[i] = cpu_to_be64(v); + } +} + +#if 0 +int main(int argc, char *argv[]) +{ + struct seckey k; + + k.u.be64[0] = cpu_to_be64(0xFFFFFFFFFFFFFFFFULL); + k.u.be64[1] = cpu_to_be64(0xFFFFFFFFFFFFFFFEULL); + k.u.be64[2] = cpu_to_be64(0xBAAEDCE6AF48A03BULL); + k.u.be64[3] = cpu_to_be64(0xBFD25E8CD0364141ULL); + flip_key(&k); + assert(k.u.be64[0] == 0); + assert(k.u.be64[1] == 0); + assert(k.u.be64[2] == 0); + assert(k.u.be64[3] == 0); + flip_key(&k); + assert(k.u.be64[0] == cpu_to_be64(0xFFFFFFFFFFFFFFFFULL)); + assert(k.u.be64[1] == cpu_to_be64(0xFFFFFFFFFFFFFFFEULL)); + assert(k.u.be64[2] == cpu_to_be64(0xBAAEDCE6AF48A03BULL)); + assert(k.u.be64[3] == cpu_to_be64(0xBFD25E8CD0364141ULL)); + + k.u.be64[0] = cpu_to_be64(0xFFFFFFFFFFFFFFFFULL); + k.u.be64[1] = cpu_to_be64(0xFFFFFFFFFFFFFFFEULL); + k.u.be64[2] = cpu_to_be64(0xBAAEDCE6AF48A03BULL); + k.u.be64[3] = cpu_to_be64(0xBFD25E8CD0364142ULL); + flip_key(&k); + assert(k.u.be64[0] == 0xFFFFFFFFFFFFFFFFULL); + assert(k.u.be64[1] == 0xFFFFFFFFFFFFFFFFULL); + assert(k.u.be64[2] == 0xFFFFFFFFFFFFFFFFULL); + assert(k.u.be64[3] == 0xFFFFFFFFFFFFFFFFULL); + flip_key(&k); + assert(k.u.be64[0] == cpu_to_be64(0xFFFFFFFFFFFFFFFFULL)); + assert(k.u.be64[1] == cpu_to_be64(0xFFFFFFFFFFFFFFFEULL)); + assert(k.u.be64[2] == cpu_to_be64(0xBAAEDCE6AF48A03BULL)); + assert(k.u.be64[3] == cpu_to_be64(0xBFD25E8CD0364142ULL)); + + return 0; +} +#endif + +static void random_key(secp256k1_context *ctx, + struct seckey *seckey, secp256k1_pubkey *pkey) { do { - random_bytes(seckey->k.u.u8, sizeof(seckey->k)); - } while (!secp256k1_ec_pubkey_create(ctx, pubkey, seckey->k.u.u8)); + random_bytes(seckey->u.u8, sizeof(seckey->u)); + } while (!secp256k1_ec_pubkey_create(ctx, pkey, seckey->u.u8)); +} + +/* We don't want to spend a byte encoding sign, so make sure it's 0x2 */ +static void gen_keys(secp256k1_context *ctx, + struct seckey *seckey, struct onion_pubkey *pubkey) +{ + unsigned char tmp[33]; + secp256k1_pubkey pkey; + size_t len; + + random_key(ctx, seckey, &pkey); + + secp256k1_ec_pubkey_serialize(ctx, tmp, &len, &pkey, + SECP256K1_EC_COMPRESSED); + assert(len == sizeof(tmp)); + if (tmp[0] == 0x3) + flip_key(seckey); + memcpy(pubkey, tmp+1, sizeof(*pubkey)); } /* @@ -145,33 +216,29 @@ static void gen_keys(secp256k1_context *ctx, #define MAX_HOPS 20 struct hop { - struct sha256 hmac; - /* FIXME: Must use parse/serialize functions. */ - secp256k1_pubkey pubkey; unsigned char msg[MESSAGE_SIZE]; + struct onion_pubkey pubkey; + struct sha256 hmac; }; struct onion { struct hop hop[MAX_HOPS]; }; +/* We peel from the back. */ +static struct hop *myhop(const struct onion *onion) +{ + return (struct hop *)&onion->hop[MAX_HOPS-1]; +} + static bool aes_encrypt(void *dst, const void *src, size_t len, const struct enckey *enckey, const struct iv *iv) { -#ifdef EXPORT_FRIENDLY - unsigned char *dptr = dst; - const unsigned char *sptr = memcheck(src, len); - size_t i; - - for (i = 0; i < len; i++) - dptr[i] = sptr[i] + iv->iv[0] + i / sizeof(struct hop); - return true; -#else EVP_CIPHER_CTX evpctx; int outlen; /* Counter mode allows parallelism in future. */ - if (EVP_EncryptInit(&evpctx, EVP_aes_256_ctr(), + if (EVP_EncryptInit(&evpctx, EVP_aes_128_ctr(), memcheck(enckey->k.u.u8, sizeof(enckey->k)), memcheck(iv->iv, sizeof(iv->iv))) != 1) return false; @@ -187,26 +254,16 @@ static bool aes_encrypt(void *dst, const void *src, size_t len, return false; assert(outlen == 0); return true; -#endif } static bool aes_decrypt(void *dst, const void *src, size_t len, const struct enckey *enckey, const struct iv *iv) { -#ifdef EXPORT_FRIENDLY - unsigned char *dptr = dst; - const unsigned char *sptr = memcheck(src, len); - size_t i; - - for (i = 0; i < len; i++) - dptr[i] = sptr[i] - iv->iv[0] - i / sizeof(struct hop); - return true; -#else EVP_CIPHER_CTX evpctx; int outlen; /* Counter mode allows parallelism in future. */ - if (EVP_DecryptInit(&evpctx, EVP_aes_256_ctr(), + if (EVP_DecryptInit(&evpctx, EVP_aes_128_ctr(), memcheck(enckey->k.u.u8, sizeof(enckey->k)), memcheck(iv->iv, sizeof(iv->iv))) != 1) return false; @@ -222,7 +279,6 @@ static bool aes_decrypt(void *dst, const void *src, size_t len, return false; assert(outlen == 0); return true; -#endif } void dump_contents(const void *data, size_t n) @@ -237,28 +293,26 @@ void dump_contents(const void *data, size_t n) } } -static bool decrypt_padding(struct hop *padding, size_t nhops, - const struct enckey *enckey, - const struct iv *iv) +static bool aes_encrypt_offset(size_t offset, + void *dst, const void *src, size_t len, + const struct enckey *enckey, + const struct iv *iv) { /* * FIXME: This would be easier if we could set the counter; instead - * we simulate it by decrypting junk before the actual padding. + * we simulate it by encrypting junk before the actual data. */ - struct hop tmp[MAX_HOPS]; - + char tmp[offset + len]; + /* Keep valgrind happy. */ - memset(tmp, 0, (MAX_HOPS - nhops) * sizeof(struct hop)); - - memcpy(tmp + MAX_HOPS - nhops, padding, nhops * sizeof(struct hop)); + memset(tmp, 0, offset); + memcpy(tmp + offset, src, len); - /* FIXME: Assumes we are allowed to decrypt in place! */ - if (!aes_decrypt((char *)tmp + offsetof(struct hop, msg), - (char *)tmp + offsetof(struct hop, msg), - sizeof(tmp) - offsetof(struct hop, msg), enckey, iv)) + /* FIXME: Assumes we are allowed to encrypt in place! */ + if (!aes_encrypt(tmp, tmp, offset+len, enckey, iv)) return false; - memcpy(padding, tmp + MAX_HOPS - nhops, nhops * sizeof(struct hop)); + memcpy(dst, tmp + offset, len); return true; } @@ -277,32 +331,33 @@ static void make_hmac(const struct hop *hops, size_t num_hops, const struct hmackey *hmackey, struct sha256 *hmac) { -#ifdef NO_HMAC - /* Copy first byte of message on each hop. */ - size_t i; - - memset(hmac, 0, sizeof(*hmac)); - for (i = 0; i < MAX_HOPS; i++) { - if (i < num_hops) - hmac->u.u8[i] = hops[i].msg[0]; - else - hmac->u.u8[i] = padding[i - num_hops].msg[0]; - } -#else HMAC_CTX ctx; size_t len, padlen; - /* Calculate HMAC of pubkey onwards, plus padding. */ + /* Calculate HMAC of padding then onion up to and including pubkey. */ HMAC_CTX_init(&ctx); HMAC_Init_ex(&ctx, memcheck(hmackey->k.u.u8, sizeof(hmackey->k)), sizeof(hmackey->k), EVP_sha256(), NULL); - len = num_hops*sizeof(struct hop) - offsetof(struct hop, pubkey); - HMAC_Update(&ctx, memcheck((unsigned char *)hops + offsetof(struct hop, pubkey), - len), len); padlen = (MAX_HOPS - num_hops) * sizeof(struct hop); HMAC_Update(&ctx, memcheck((unsigned char *)padding, padlen), padlen); + len = num_hops*sizeof(struct hop) - sizeof(hops->hmac); + HMAC_Update(&ctx, memcheck((unsigned char *)hops, len), len); HMAC_Final(&ctx, hmac->u.u8, NULL); -#endif +} + +void _dump_hex(unsigned char *x, size_t s) { + printf(" "); + while (s > 0) { + printf("%02x", *x); + x++; s--; + } +} +#define dump_hex(x) _dump_hex((void*)&x, sizeof(x)) +void dump_pkey(secp256k1_context *ctx, secp256k1_pubkey pkey) { + unsigned char tmp[65]; + size_t len; + secp256k1_ec_pubkey_serialize(ctx, tmp, &len, &pkey, 0); + dump_hex(tmp); } static bool check_hmac(struct onion *onion, const struct hmackey *hmackey) @@ -310,7 +365,7 @@ static bool check_hmac(struct onion *onion, const struct hmackey *hmackey) struct sha256 hmac; make_hmac(onion->hop, MAX_HOPS, NULL, hmackey, &hmac); - return CRYPTO_memcmp(&hmac, &onion->hop[0].hmac, sizeof(hmac)) == 0; + return CRYPTO_memcmp(&hmac, &myhop(onion)->hmac, sizeof(hmac)) == 0; } bool create_onion(const secp256k1_pubkey pubkey[], @@ -319,37 +374,37 @@ bool create_onion(const secp256k1_pubkey pubkey[], struct onion *onion) { int i; - struct seckey *seckeys = tal_arr(NULL, struct seckey, num); - secp256k1_pubkey *pubkeys = tal_arr(seckeys, secp256k1_pubkey, num); - struct enckey *enckeys = tal_arr(seckeys, struct enckey, num); - struct hmackey *hmackeys = tal_arr(seckeys, struct hmackey, num); - struct iv *ivs = tal_arr(seckeys, struct iv, num); - struct iv *pad_ivs = tal_arr(seckeys, struct iv, num); - struct hop **padding = tal_arr(seckeys, struct hop *, num); - struct hop **hops = tal_arr(seckeys, struct hop *, num); + struct seckey seckeys[MAX_HOPS]; + struct onion_pubkey pubkeys[MAX_HOPS]; + struct enckey enckeys[MAX_HOPS]; + struct hmackey hmackeys[MAX_HOPS]; + struct iv ivs[MAX_HOPS]; + struct iv pad_ivs[MAX_HOPS]; + HMAC_CTX padding_hmac[MAX_HOPS]; + struct hop padding[MAX_HOPS]; size_t junk_hops; - secp256k1_context *ctx; + secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); bool ok = false; if (num > MAX_HOPS) goto fail; - ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); - + /* FIXME: I think it would be safe to reuse a single disposable key + * here? */ /* First generate all the keys. */ for (i = 0; i < num; i++) { unsigned char secret[32]; gen_keys(ctx, &seckeys[i], &pubkeys[i]); + /* Make shared secret. */ - if (!secp256k1_ecdh(ctx, secret, &pubkey[i], seckeys[i].k.u.u8)) + if (!secp256k1_ecdh(ctx, secret, &pubkey[i], seckeys[i].u.u8)) goto fail; hmackeys[i] = hmackey_from_secret(memcheck(secret, 32)); enckeys[i] = enckey_from_secret(secret); - ivs[i] = iv_from_secret(secret, i); - pad_ivs[i] = pad_iv_from_secret(secret, i); + ivs_from_secret(secret, &ivs[i], &pad_ivs[i]); } /* @@ -359,18 +414,24 @@ bool create_onion(const secp256k1_pubkey pubkey[], * and "decrypted" by the others. So we have to generate that * forwards. */ - for (i = 1; i < num; i++) { - /* Each one has 1 padding from previous. */ - padding[i] = tal_arr(padding, struct hop, i); - - /* Copy padding from previous node. */ - memcpy(padding[i], padding[i-1], sizeof(struct hop)*(i-1)); - /* Previous node "decrypts" it before handing to us */ - if (!decrypt_padding(padding[i], i-1, - &enckeys[i-1], &ivs[i-1])) - goto fail; - /* And generates another lot of padding. */ - add_padding(padding[i]+i-1, &enckeys[i-1], &pad_ivs[i-1]); + for (i = 0; i < num; i++) { + if (i > 0) { + /* Previous node decrypts padding before passing on. */ + aes_decrypt(padding, padding, sizeof(struct hop)*(i-1), + &enckeys[i-1], &ivs[i-1]); + memmove(padding + 1, padding, + sizeof(struct hop)*(i-1)); + } + /* And generates more padding for next node. */ + add_padding(&padding[0], &enckeys[i-1], &pad_ivs[i-1]); + HMAC_CTX_init(&padding_hmac[i]); + HMAC_Init_ex(&padding_hmac[i], + hmackeys[i].k.u.u8, sizeof(hmackeys[i].k), + EVP_sha256(), NULL); + HMAC_Update(&padding_hmac[i], + memcheck((unsigned char *)padding, + i * sizeof(struct hop)), + i * sizeof(struct hop)); } /* @@ -380,77 +441,83 @@ bool create_onion(const secp256k1_pubkey pubkey[], /* Unused hops filled with random, so even recipient can't tell * how many were used. */ junk_hops = MAX_HOPS - num; + random_bytes(onion->hop, junk_hops * sizeof(struct hop)); for (i = num - 1; i >= 0; i--) { - size_t other_hops; - struct hop *myonion; + size_t other_hops, len; + struct hop *myhop; other_hops = num - i - 1 + junk_hops; - myonion = hops[i] = tal_arr(hops, struct hop, 1 + other_hops); - if (i == num - 1) { - /* Fill with junk. */ - random_bytes(myonion + 1, - other_hops * sizeof(struct hop)); - } else { - /* Copy from next hop. */ - memcpy(myonion + 1, hops[i+1], - other_hops * sizeof(struct hop)); - } + + /* Our entry is at tail of onion. */ + myhop = onion->hop + other_hops; /* Now populate our hop. */ - myonion->pubkey = pubkeys[i]; + myhop->pubkey = pubkeys[i]; /* Set message. */ assert(strlen(msg[i]) < MESSAGE_SIZE); - memset(myonion->msg, 0, MESSAGE_SIZE); - strcpy((char *)myonion->msg, msg[i]); - - /* Encrypt whole thing from message onwards. */ - if (!aes_encrypt(&myonion->msg, &myonion->msg, - (1 + other_hops) * sizeof(struct hop) - - offsetof(struct hop, msg), - &enckeys[i], &ivs[i])) + memset(myhop->msg, 0, MESSAGE_SIZE); + strcpy((char *)myhop->msg, msg[i]); + + /* Encrypt whole thing, including our message, but we + * aware it will be offset by the prepended padding. */ + if (!aes_encrypt_offset(i * sizeof(struct hop), + onion, onion, + other_hops * sizeof(struct hop) + + sizeof(myhop->msg), + &enckeys[i], &ivs[i])) goto fail; /* HMAC covers entire thing except hmac itself. */ - make_hmac(myonion, 1 + other_hops, padding[i], - &hmackeys[i], &myonion->hmac); + len = (other_hops + 1)*sizeof(struct hop) - sizeof(myhop->hmac); + HMAC_Update(&padding_hmac[i], + memcheck((unsigned char *)onion, len), len); + HMAC_Final(&padding_hmac[i], myhop->hmac.u.u8, NULL); } - /* Transfer results to onion, for first node. */ - assert(tal_count(hops[0]) == MAX_HOPS); - memcpy(onion->hop, hops[0], sizeof(onion->hop)); ok = true; - fail: - tal_free(seckeys); secp256k1_context_destroy(ctx); return ok; } +static bool pubkey_parse(const secp256k1_context *ctx, + secp256k1_pubkey* pubkey, + struct onion_pubkey *pkey) +{ + unsigned char tmp[33]; + + tmp[0] = 0x2; + memcpy(tmp+1, pkey, sizeof(*pkey)); + return secp256k1_ec_pubkey_parse(ctx, pubkey, tmp, sizeof(tmp)); +} + /* * Decrypt onion, return true if onion->hop[0] is valid. * * Returns enckey and pad_iv for use in unwrap. */ bool decrypt_onion(const struct seckey *myseckey, struct onion *onion, - struct enckey *enckey, struct iv *pad_iv, size_t i) + struct enckey *enckey, struct iv *pad_iv) { secp256k1_context *ctx; unsigned char secret[32]; struct hmackey hmackey; struct iv iv; + secp256k1_pubkey pubkey; ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + if (!pubkey_parse(ctx, &pubkey, &myhop(onion)->pubkey)) + goto fail; + /* Extract shared secret. */ - if (!secp256k1_ecdh(ctx, secret, &onion->hop[0].pubkey, - myseckey->k.u.u8)) + if (!secp256k1_ecdh(ctx, secret, &pubkey, myseckey->u.u8)) goto fail; hmackey = hmackey_from_secret(secret); *enckey = enckey_from_secret(secret); - iv = iv_from_secret(secret, i); - *pad_iv = pad_iv_from_secret(secret, i); + ivs_from_secret(secret, &iv, pad_iv); /* Check HMAC. */ #if 0 @@ -478,9 +545,11 @@ bool decrypt_onion(const struct seckey *myseckey, struct onion *onion, if (!check_hmac(onion, &hmackey)) goto fail; - /* Decrypt everything after pubkey. */ - if (!aes_decrypt(onion->hop[0].msg, onion->hop[0].msg, - sizeof(*onion) - offsetof(struct hop, msg), + /* Decrypt everything up to pubkey. */ + /* FIXME: Assumes we can decrypt in place! */ + if (!aes_decrypt(onion, onion, + sizeof(struct hop) * (MAX_HOPS-1) + + sizeof(myhop(onion)->msg), enckey, &iv)) goto fail; @@ -496,55 +565,117 @@ fail: bool peel_onion(struct onion *onion, const struct enckey *enckey, const struct iv *pad_iv) { - /* Move next one to front. */ - memmove(&onion->hop[0], &onion->hop[1], + /* Move next one to back. */ + memmove(&onion->hop[1], &onion->hop[0], sizeof(*onion) - sizeof(onion->hop[0])); /* Add random-looking (but predictable) padding. */ - memset(&onion->hop[MAX_HOPS-1], 0, sizeof(onion->hop[MAX_HOPS-1])); - return aes_encrypt(&onion->hop[MAX_HOPS-1], &onion->hop[MAX_HOPS-1], - sizeof(onion->hop[MAX_HOPS-1]), enckey, pad_iv); + memset(&onion->hop[0], 0, sizeof(onion->hop[0])); + return aes_encrypt(&onion->hop[0], &onion->hop[0], + sizeof(onion->hop[0]), enckey, pad_iv); +} + +static bool parse_onion_pubkey(secp256k1_context *ctx, + const char *arg, secp256k1_pubkey *pubkey) +{ + unsigned char tmp[33] = { }; + + if (!hex_decode(arg, strlen(arg), tmp, sizeof(tmp))) + return false; + + return secp256k1_ec_pubkey_parse(ctx, pubkey, tmp, sizeof(tmp)); +} + +static char *make_message(secp256k1_context *ctx, + const secp256k1_pubkey *pubkey) +{ + char *m; + unsigned char tmp[33]; + size_t len; + char hexstr[hex_str_size(20)]; + + secp256k1_ec_pubkey_serialize(ctx, tmp, &len, pubkey, + SECP256K1_EC_COMPRESSED); + hex_encode(tmp+1, 20, hexstr, sizeof(hexstr)); + asprintf(&m, "Message for %s...", hexstr); + return m; } int main(int argc, char *argv[]) { secp256k1_context *ctx; - size_t i, hops; - struct seckey seckeys[MAX_HOPS]; - secp256k1_pubkey pubkeys[MAX_HOPS]; - char *msgs[MAX_HOPS]; struct onion onion; + bool generate = false, decode = false; - assert(EVP_CIPHER_iv_length(EVP_aes_256_ctr()) == sizeof(struct iv)); - - if (argc != 2) - errx(1, "Usage: %s ", argv[0]); - hops = atoi(argv[1]); - if (hops == 0 || hops > MAX_HOPS) - errx(1, "%s is invalid number of hops", argv[1]); + assert(EVP_CIPHER_iv_length(EVP_aes_128_ctr()) == sizeof(struct iv)); - ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); - for (i = 0; i < hops; i++) { - asprintf(&msgs[i], "Message to %zu", i); - gen_keys(ctx, &seckeys[i], &pubkeys[i]); - } + opt_register_noarg("--help|-h", opt_usage_and_exit, + "--generate ... OR\n" + "--decode \n" + "Either create an onion message, or decode one step", + "Print this message."); + opt_register_noarg("--generate", + opt_set_bool, &generate, + "Generate onion through the given hex pubkeys"); + opt_register_noarg("--decode", + opt_set_bool, &decode, + "Decode onion given the private key"); + opt_register_version(); + + opt_parse(&argc, argv, opt_log_stderr_exit); - if (!create_onion(pubkeys, msgs, hops, &onion)) - errx(1, "Creating onion packet failed"); + ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + if (generate) { + secp256k1_pubkey pubkeys[MAX_HOPS]; + char *msgs[MAX_HOPS]; + size_t i; + + if (argc == 1) + opt_usage_exit_fail("Expected at least one pubkey"); + if (argc-1 > MAX_HOPS) + opt_usage_exit_fail("Expected at most %u pubkeys", + MAX_HOPS); + for (i = 1; i < argc; i++) { + if (!parse_onion_pubkey(ctx, argv[i], &pubkeys[i-1])) + errx(1, "Bad pubkey '%s'", argv[i]); + msgs[i-1] = make_message(ctx, &pubkeys[i-1]); + } - /* Now parse and peel. */ - for (i = 0; i < hops; i++) { + if (!create_onion(pubkeys, msgs, argc - 1, &onion)) + errx(1, "Creating onion packet failed"); + if (!write_all(STDOUT_FILENO, &onion, sizeof(onion))) + err(1, "Writing onion packet"); + return 0; + } else if (decode) { + struct seckey seckey; + secp256k1_pubkey pubkey; struct enckey enckey; struct iv pad_iv; - printf("Decrypting with key %zi\n", i); - if (!decrypt_onion(&seckeys[i], &onion, &enckey, &pad_iv, i)) - errx(1, "Decrypting onion for hop %zi", i); - if (strcmp((char *)onion.hop[0].msg, msgs[i]) != 0) - errx(1, "Bad message for hop %zi", i); + if (argc != 2) + opt_usage_exit_fail("Expect a privkey with --decode"); + + if (!hex_decode(argv[1], strlen(argv[1]), &seckey, sizeof(seckey))) + errx(1, "Invalid private key hex '%s'", argv[1]); + if (!secp256k1_ec_pubkey_create(ctx, &pubkey, seckey.u.u8)) + errx(1, "Invalid private key '%s'", argv[1]); + + if (!read_all(STDIN_FILENO, &onion, sizeof(onion))) + errx(1, "Reading in onion"); + + if (!decrypt_onion(&seckey, &onion, &enckey, &pad_iv)) + errx(1, "Failed decrypting onion for '%s'", argv[1]); + if (strncmp((char *)myhop(&onion)->msg, make_message(ctx, &pubkey), + sizeof(myhop(&onion)->msg))) + errx(1, "Bad message '%s'", (char *)myhop(&onion)->msg); if (!peel_onion(&onion, &enckey, &pad_iv)) - errx(1, "Peeling onion for hop %zi", i); - } + errx(1, "Peeling onion for '%s'", argv[1]); + if (!write_all(STDOUT_FILENO, &onion, sizeof(onion))) + err(1, "Writing onion packet"); + return 0; + } else + opt_usage_exit_fail("Need --decode or --generate"); + secp256k1_context_destroy(ctx); return 0; } diff --git a/test/test_onion.py b/test/test_onion.py new file mode 100644 index 000000000..a09983fac --- /dev/null +++ b/test/test_onion.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python + +import argparse +import sys +import time + +from hashlib import sha256 +from binascii import hexlify, unhexlify +import hmac +import random + +from cryptography.hazmat.primitives.ciphers import Cipher, modes, algorithms +from cryptography.hazmat.primitives.ciphers.algorithms import AES +from cryptography.hazmat.primitives.ciphers.modes import CTR +from cryptography.hazmat.backends import default_backend +# http://cryptography.io + +from pyelliptic import ecc + +class MyEx(Exception): pass + +def hmac_sha256(k, m): + return hmac.new(k, m, sha256).digest() + + + + + + +## pyelliptic doesn't support compressed pubkey representations +## so we have to add some code... +from pyelliptic.openssl import OpenSSL +import ctypes + +OpenSSL.EC_POINT_set_compressed_coordinates_GFp = \ + OpenSSL._lib.EC_POINT_set_compressed_coordinates_GFp +OpenSSL.EC_POINT_set_compressed_coordinates_GFp.restype = ctypes.c_int +OpenSSL.EC_POINT_set_compressed_coordinates_GFp.argtypes = [ + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p] + +def ecc_ecdh_key(sec, pub): + assert isinstance(sec, ecc.ECC) + if isinstance(pub, ecc.ECC): + pub = pub.get_pubkey() + #return sec.get_ecdh_key(pub) + + pubkey_x, pubkey_y = ecc.ECC._decode_pubkey(pub, 'binary') + + other_key = other_pub_key_x = other_pub_key_y = other_pub_key = None + own_priv_key = res = res_x = res_y = None + try: + other_key = OpenSSL.EC_KEY_new_by_curve_name(sec.curve) + if other_key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ... " + OpenSSL.get_error()) + + other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) + other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) + + other_group = OpenSSL.EC_KEY_get0_group(other_key) + other_pub_key = OpenSSL.EC_POINT_new(other_group) + if (other_pub_key == None): + raise Exception("[OpenSSl] EC_POINT_new FAIL ... " + OpenSSL.get_error()) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, + other_pub_key, + other_pub_key_x, + other_pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ..." + OpenSSL.get_error()) + + own_priv_key = OpenSSL.BN_bin2bn(sec.privkey, len(sec.privkey), 0) + + res = OpenSSL.EC_POINT_new(other_group) + if (OpenSSL.EC_POINT_mul(other_group, res, 0, other_pub_key, own_priv_key, 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_mul FAIL ..." + OpenSSL.get_error()) + + res_x = OpenSSL.BN_new() + res_y = OpenSSL.BN_new() + + if (OpenSSL.EC_POINT_get_affine_coordinates_GFp(other_group, res, + res_x, + res_y, 0 + )) == 0: + raise Exception( + "[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ... " + OpenSSL.get_error()) + + resx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(res_x)) + resy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(res_y)) + + OpenSSL.BN_bn2bin(res_x, resx) + resx = resx.raw + OpenSSL.BN_bn2bin(res_y, resy) + resy = resy.raw + + return resx, resy + + finally: + if other_key: OpenSSL.EC_KEY_free(other_key) + if other_pub_key_x: OpenSSL.BN_free(other_pub_key_x) + if other_pub_key_y: OpenSSL.BN_free(other_pub_key_y) + if other_pub_key: OpenSSL.EC_POINT_free(other_pub_key) + if own_priv_key: OpenSSL.BN_free(own_priv_key) + if res: OpenSSL.EC_POINT_free(res) + if res_x: OpenSSL.BN_free(res_x) + if res_y: OpenSSL.BN_free(res_y) + +def get_pos_y_for_x(pubkey_x, yneg=0): + key = pub_key = pub_key_x = pub_key_y = None + try: + key = OpenSSL.EC_KEY_new_by_curve_name(OpenSSL.get_curve('secp256k1')) + group = OpenSSL.EC_KEY_get0_group(key) + pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) + pub_key = OpenSSL.EC_POINT_new(group) + + if OpenSSL.EC_POINT_set_compressed_coordinates_GFp(group, pub_key, + pub_key_x, yneg, 0) == 0: + raise Exception("[OpenSSL] EC_POINT_set_compressed_coordinates_GFp FAIL ... " + OpenSSL.get_error()) + + + pub_key_y = OpenSSL.BN_new() + if (OpenSSL.EC_POINT_get_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, 0 + )) == 0: + raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ... " + OpenSSL.get_error()) + + pubkeyy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_y)) + OpenSSL.BN_bn2bin(pub_key_y, pubkeyy) + pubkeyy = pubkeyy.raw + field_size = OpenSSL.EC_GROUP_get_degree(OpenSSL.EC_KEY_get0_group(key)) + secret_len = int((field_size + 7) / 8) + if len(pubkeyy) < secret_len: + pubkeyy = pubkeyy.rjust(secret_len, b'\0') + return pubkeyy + finally: + if key is not None: OpenSSL.EC_KEY_free(key) + if pub_key is not None: OpenSSL.EC_POINT_free(pub_key) + if pub_key_x is not None: OpenSSL.BN_free(pub_key_x) + if pub_key_y is not None: OpenSSL.BN_free(pub_key_y) + +def ec_decompress(pubkey, curve='secp256k1'): + if pubkey[0] == '\x02' or pubkey[0] == '\x03': + yneg = ord(pubkey[0]) & 1 + pubkey = "\x04" + pubkey[1:] + get_pos_y_for_x(pubkey[1:], yneg=yneg) + elif pubkey[0] == '\x04': + pass + else: + raise Exception("Unrecognised pubkey format: %s" % (pubkey,)) + return pubkey + +class Onion(object): + HMAC_LEN = 32 + PKEY_LEN = 32 + MSG_LEN = 128 + ZEROES = b"\x00" * (HMAC_LEN + PKEY_LEN + MSG_LEN) + + @staticmethod + def tweak_sha(sha, d): + sha = sha.copy() + sha.update(d) + return sha.digest() + + @classmethod + def get_ecdh_secrets(cls, sec, pkey_x, pkey_y): + pkey = unhexlify('04') + pkey_x + pkey_y + tmp_key = ecc.ECC(curve='secp256k1', pubkey=pkey) + sec_x, sec_y = ecc_ecdh_key(sec, tmp_key) + + b = '\x02' if ord(sec_y[-1]) % 2 == 0 else '\x03' + sec = sha256(sha256(b + sec_x).digest()) + + enckey = cls.tweak_sha(sec, b'\x00')[:16] + hmac = cls.tweak_sha(sec, b'\x01') + ivs = cls.tweak_sha(sec, b'\x02') + iv, pad_iv = ivs[:16], ivs[16:] + + return enckey, hmac, iv, pad_iv + + def enc_pad(self, enckey, pad_iv): + aes = Cipher(AES(enckey), CTR(pad_iv), + default_backend()).encryptor() + return aes.update(self.ZEROES) + +class OnionDecrypt(Onion): + def __init__(self, onion, my_ecc): + self.my_ecc = my_ecc + + hmac_end = len(onion) + pkey_end = hmac_end - self.HMAC_LEN + self.msg_end = pkey_end - self.PKEY_LEN + self.fwd_end = self.msg_end - self.MSG_LEN + + self.onion = onion + self.pkey = onion[self.msg_end:pkey_end] + self.hmac = onion[pkey_end:hmac_end] + + self.get_secrets() + + def decrypt(self): + pad = self.enc_pad(self.enckey, self.pad_iv) + + aes = Cipher(AES(self.enckey), CTR(self.iv), + default_backend()).decryptor() + self.fwd = pad + aes.update(self.onion[:self.fwd_end]) + self.msg = aes.update(self.onion[self.fwd_end:self.msg_end]) + + def get_secrets(self): + pkey_x = self.pkey + pkey_y = get_pos_y_for_x(pkey_x) # always positive by design + enckey, hmac, iv, pad_iv = self.get_ecdh_secrets(self.my_ecc, pkey_x, pkey_y) + if not self.check_hmac(hmac): + raise Exception("HMAC did not verify") + self.enckey = enckey + self.iv = iv + self.pad_iv = pad_iv + + def check_hmac(self, hmac_key): + calc = hmac_sha256(hmac_key, self.onion[:-self.HMAC_LEN]) + return calc == self.hmac + +class OnionEncrypt(Onion): + def __init__(self, msgs, pubkeys): + assert len(msgs) == len(pubkeys) + assert 0 < len(msgs) <= 20 + assert all( len(m) <= self.MSG_LEN for m in msgs ) + + msgs = [m + "\0"*(self.MSG_LEN - len(m)) for m in msgs] + pubkeys = [ecc.ECC(pubkey=pk, curve='secp256k1') for pk in pubkeys] + n = len(msgs) + + tmpkeys = [] + tmppubkeys = [] + for i in range(n): + while True: + t = ecc.ECC(curve='secp256k1') + if ord(t.pubkey_y[-1]) % 2 == 0: + break + # or do the math to "flip" the secret key and pub key + tmpkeys.append(t) + tmppubkeys.append(t.pubkey_x) + + enckeys, hmacs, ivs, pad_ivs = zip(*[self.get_ecdh_secrets(tmpkey, pkey.pubkey_x, pkey.pubkey_y) + for tmpkey, pkey in zip(tmpkeys, pubkeys)]) + + # padding takes the form: + # E_(n-1)(0000s) + # D_(n-1)( + # E(n-2)(0000s) + # D(n-2)( + # ... + # ) + # ) + + padding = "" + for i in range(n-1): + pad = self.enc_pad(enckeys[i], pad_ivs[i]) + aes = Cipher(AES(enckeys[i]), CTR(ivs[i]), + default_backend()).decryptor() + padding = pad + aes.update(padding) + + if n < 20: + padding += str(bytearray(random.getrandbits(8) + for _ in range(len(self.ZEROES) * (20-n)))) + + # to encrypt the message we need to bump the counter past all + # the padding, then just encrypt the final message + aes = Cipher(AES(enckeys[-1]), CTR(ivs[-1]), + default_backend()).encryptor() + aes.update(padding) # don't care about cyphertext + msgenc = aes.update(msgs[-1]) + + msgenc = padding + msgenc + tmppubkeys[-1] + del padding + msgenc += hmac_sha256(hmacs[-1], msgenc) + + # *PHEW* + # now iterate + + for i in reversed(range(n-1)): + # drop the padding this node will add + msgenc = msgenc[len(self.ZEROES):] + # adding the msg + msgenc += msgs[i] + # encrypt it + aes = Cipher(AES(enckeys[i]), CTR(ivs[i]), + default_backend()).encryptor() + msgenc = aes.update(msgenc) + # add the tmp key + msgenc += tmppubkeys[i] + # add the hmac + msgenc += hmac_sha256(hmacs[i], msgenc) + self.onion = msgenc + +def generate(args): + server_keys = [] + msgs = [] + for k in args.pubkeys: + k = unhexlify(k) + msgs.append("Message for %s..." % (hexlify(k[1:21]),)) + k = ec_decompress(k) + server_keys.append(k) + o = OnionEncrypt(msgs, server_keys) + sys.stdout.write(o.onion) + return + +def decode(args): + msg = sys.stdin.read() + key = ecc.ECC(privkey=unhexlify(args.seckey), + pubkey=ec_decompress(unhexlify(args.pubkey)), + curve='secp256k1') + o = OnionDecrypt(msg, key) + o.decrypt() + #sys.stderr.write("Message: \"%s\"\n" % (o.msg,)) + want_msg = "Message for %s..." % (args.pubkey[2:42]) + if o.msg != want_msg + "\0"*(Onion.MSG_LEN - len(want_msg)): + raise Exception("Unexpected message: \"%s\" (wanted: %s)" % (o.msg, want_msg)) + + sys.stdout.write(o.fwd) + +def main(argv): + parser = argparse.ArgumentParser(description="Process some integers.") + sp = parser.add_subparsers() + p = sp.add_parser("generate") + p.add_argument("pubkeys", nargs='+', help="public keys of recipients") + p.set_defaults(func=generate) + + p = sp.add_parser("decode") + p.add_argument("seckey", help="secret key for router") + p.add_argument("pubkey", help="public key for router") + p.set_defaults(func=decode) + + args = parser.parse_args(argv) + + return args.func(args) + + + + +if __name__ == "__main__": + main(sys.argv[1:]) + sys.exit(0) +