Use hash instead of random for signing hash data

This makes PrivateKey store both the key followed by the hash.  For
PrivateKeys based on SecretKeys this just means the second half of the
SHA-512 of the seed, and makes a PrivateKey constructed from a SecretKey
give an identical signature to signing directly with sodium.

For derived keys we use a ShortHash of the root key's signing hash
concatenated with the publicly known hash value, so that our derived key
signing hash will be different from the root signing hash and also
different for different derivation parameters.

This also changed one of the asserts in crypto_noop, but upon closer
inspection the copying of the secret key into the signature seems really
wrong, so just changed them to fill with 0s.
This commit is contained in:
Jason Rhinelander 2020-01-31 16:38:08 -04:00
parent fe8a72750a
commit cd8f26deff
5 changed files with 117 additions and 61 deletions

View File

@ -179,24 +179,23 @@ namespace llarp
}
bool
CryptoLibSodium::sign(Signature &sig, const PrivateKey &secret,
CryptoLibSodium::sign(Signature &sig, const PrivateKey& privkey,
const llarp_buffer_t &buf)
{
PubKey pubkey;
secret.toPublic(pubkey);
privkey.toPublic(pubkey);
crypto_hash_sha512_state hs;
unsigned char nonce[64];
unsigned char hram[64];
unsigned char mulres[32];
unsigned char r_hash_input[32];
randombytes_buf(r_hash_input, 32);
// r = H(H(k) || M) where here H(k) is random bytes
// r = H(s || M) where here s is pseudorandom bytes typically generated as part of hashing the
// seed (i.e. [a,s] = H(k)), but for derived PrivateKeys will come from a hash of the root
// key's s concatenated with the derivation hash.
crypto_hash_sha512_init(&hs);
crypto_hash_sha512_update(&hs, r_hash_input, 32);
crypto_hash_sha512_update(&hs, privkey.data() + 32, 32);
crypto_hash_sha512_update(&hs, buf.base, buf.sz);
crypto_hash_sha512_final(&hs, nonce);
crypto_core_ed25519_scalar_reduce(nonce, nonce);
@ -215,10 +214,9 @@ namespace llarp
// S = r + H(R || A || M) * s, so sig = (R || S)
crypto_core_ed25519_scalar_reduce(hram, hram);
crypto_core_ed25519_scalar_mul(mulres, hram, secret.data());
crypto_core_ed25519_scalar_mul(mulres, hram, privkey.data());
crypto_core_ed25519_scalar_add(sig.data() + 32, mulres, nonce);
sodium_memzero(r_hash_input, sizeof r_hash_input);
sodium_memzero(nonce, sizeof nonce);
return true;
@ -260,20 +258,23 @@ namespace llarp
return other == key;
}
constexpr static char derived_key_hash_str[161] = "just imagine what would happen if we all decided to understand. you can't in the and by be or then before so just face it this text hurts to read? lokinet yolo!";
template < typename K >
static bool
make_scalar(byte_t *out, const K &k, uint64_t i)
make_scalar(AlignedBuffer<32>& out, const K &k, uint64_t i)
{
// b = i || k
std::array< byte_t, K::SIZE + sizeof(uint64_t) > buf;
htole64buf(buf.data(), i);
std::copy_n(k.begin(), K::SIZE, buf.begin() + sizeof(i));
LongHash h;
// b = BLIND-STRING || k || i
std::array< byte_t, 160 + K::SIZE + sizeof(uint64_t) > buf;
std::copy(derived_key_hash_str, derived_key_hash_str + 160, buf.begin());
std::copy(k.begin(), k.end(), buf.begin() + 160);
htole64buf(buf.data() + 160 + K::SIZE, i);
// n = H(b)
if(not hash(h.data(), llarp_buffer_t(buf)))
return false;
// return make_point(n)
return crypto_core_ed25519_from_uniform(out, h.data()) != -1;
// h = make_point(n)
ShortHash n;
return -1 != crypto_generichash_blake2b(n.data(), ShortHash::SIZE,
buf.data(), buf.size(), nullptr, 0)
&& -1 != crypto_core_ed25519_from_uniform(out.data(), n.data());
}
static AlignedBuffer< 32 > zero;
@ -282,11 +283,11 @@ namespace llarp
CryptoLibSodium::derive_subkey(PubKey &out_pubkey, const PubKey &root_pubkey,
uint64_t key_n, const AlignedBuffer<32>* hash)
{
// scalar h = H( in_k || root_pubkey )
// scalar h = H( BLIND-STRING || root_pubkey || key_n )
AlignedBuffer< 32 > h;
if (hash)
h = *hash;
else if(not make_scalar(h.data(), root_pubkey, key_n))
else if(not make_scalar(h, root_pubkey, key_n))
{
LogError("cannot make scalar");
return false;
@ -307,27 +308,34 @@ namespace llarp
//
// The basic idea is:
//
// h - hash dependent on the `key_n` value.
// h = H( BLIND-STRING || A || key_n )
// a - private key
// A = aB - public key
// s - signing hash
// a' = ah - derived private key
// A' = a'B = (ah)B - derived public key
// s' = H(h || s) - derived signing hash
//
// libsodium throws some wrenches in the mechanics which are a nuisance, the biggest of which
// is that sodium's secret key is *not* `a`; rather it is the seed. If you want to get the
// private key (i.e. "a"), you need to SHA-512 hash it and then clamp that.
//
// This also makes signature verification harder: we can't just use sodium's verify function
// This also makes signature verification harder: we can't just use sodium's sign function
// because it wants to be given the seed rather than the private key, and moreover we can't
// actually *get* the seed to make libsodium happy because we only have `ah` above.
// actually *get* the seed to make libsodium happy because we only have `ah` above; thus we
// reimplemented most of sodium's detached signing function but without the hash step.
//
// Lastly, for the signing hash s', we need some value that is both different from the root s
// but also unknowable from the public key (since otherwise `r` in the signing function would
// be known), so we generate it from a hash of `h` and the root key's (psuedorandom) signing
// hash, `s`.
//
const auto root_pubkey = root_key.toPublic();
// scalar h = H( in_k || root_pubkey )
AlignedBuffer< 32 > h;
if (hash)
h = *hash;
else if(not make_scalar(h.data(), root_pubkey, key_n))
else if(not make_scalar(h, root_pubkey, key_n))
{
LogError("cannot make scalar");
return false;
@ -344,6 +352,13 @@ namespace llarp
// a' = ha
crypto_core_ed25519_scalar_mul(out_key.data(), h.data(), a.data());
// s' = H(h || s)
std::array<byte_t, 64> buf;
std::copy(h.begin(), h.end(), buf.begin());
std::copy(a.begin() + 32, a.end(), buf.begin() + 32);
return -1 != crypto_generichash_blake2b(out_key.data() + 32, 32,
buf.data(), buf.size(), nullptr, 0);
return true;
}

View File

@ -106,19 +106,16 @@ namespace llarp
}
bool
sign(Signature &sig, const SecretKey &key, const llarp_buffer_t &) override
sign(Signature &sig, const SecretKey &, const llarp_buffer_t &) override
{
static_assert(Signature::SIZE == SecretKey::SIZE, "");
std::copy(key.begin(), key.end(), sig.begin());
std::fill(sig.begin(), sig.end(), 0);
return true;
}
bool
sign(Signature &sig, const PrivateKey &key, const llarp_buffer_t &) override
sign(Signature &sig, const PrivateKey &, const llarp_buffer_t &) override
{
static_assert(Signature::SIZE == PrivateKey::SIZE * 2, "");
std::copy(key.begin(), key.end(), sig.begin());
std::copy(key.begin(), key.end(), sig.begin() + 32);
std::fill(sig.begin(), sig.end(), 0);
return true;
}

View File

@ -69,15 +69,15 @@ namespace llarp
bool
SecretKey::toPrivate(PrivateKey& key) const
{
// libsodium and ref10 calculate a 512-bit hash, but then only use 256 bits (32 bytes) of it for
// the private key. Because yeah.
// Ed25519 calculates a 512-bit hash from the seed; the first half (clamped) is the private key;
// the second half is the hash that gets used in signing.
unsigned char h[crypto_hash_sha512_BYTES];
if (crypto_hash_sha512(h, data(), 32) < 0)
return false;
h[0] &= 248;
h[31] &= 63;
h[31] |= 64;
std::memcpy(key.data(), h, 32);
std::memcpy(key.data(), h, 64);
return true;
}

View File

@ -136,19 +136,19 @@ namespace llarp
return out << "[secretkey]";
}
/// PrivateKey is similar to SecretKey except that it only stores the private key value
/// itself unlike SecretKey which stores the seed from which the private key value is generated.
/// This is intended for use with derived keys, where we can derive the private key but not the
/// seed.
struct PrivateKey final : public AlignedBuffer< 32 >
/// PrivateKey is similar to SecretKey except that it only stores the private key value and a
/// hash, unlike SecretKey which stores the seed from which the private key and hash value are
/// generated. This is primarily intended for use with derived keys, where we can derive the
/// private key but not the seed.
struct PrivateKey final : public AlignedBuffer< 64 >
{
PrivateKey() = default;
explicit PrivateKey(const byte_t *ptr) : AlignedBuffer< 32 >(ptr)
explicit PrivateKey(const byte_t *ptr) : AlignedBuffer< 64 >(ptr)
{
}
explicit PrivateKey(const AlignedBuffer< 32 > &seed) : AlignedBuffer< 32>(seed) {}
explicit PrivateKey(const AlignedBuffer< 64 > &key_and_hash) : AlignedBuffer< 64 >(key_and_hash) {}
std::ostream &
print(std::ostream &stream, int level, int spaces) const

View File

@ -147,28 +147,31 @@ struct RealCryptographyTest : public ::testing::Test
TEST_F(RealCryptographyTest, TestKnownDerivation)
{
// These values came out of Tor's test code, so that we can confirm we are doing the same blinding
// subkey crypto math as Tor. Our hash value is generated differently so we use the hash from a
// Tor random test suite run.
// These values came out of a run of Tor's test code, so that we can confirm we are doing the same
// blinding subkey crypto math as Tor. Our hash value is generated differently so we use the hash
// from a Tor random test suite run.
AlignedBuffer<32> seed{{
0x11, 0x68, 0xae, 0xa6, 0x62, 0x26, 0x6c, 0x53, 0x69, 0x9f, 0xe7, 0xd9, 0xbb, 0xff, 0xf6, 0x8e,
0x58, 0x22, 0xde, 0x90, 0x4b, 0x91, 0x28, 0x5a, 0x7c, 0x41, 0xcc, 0x7c, 0x36, 0xb4, 0xf5, 0xa0 }};
AlignedBuffer<32> root_key_data{{
0x40, 0x64, 0x32, 0x11, 0x19, 0xfc, 0xe8, 0x27, 0x9d, 0x3f, 0xd6, 0xe9, 0xc8, 0x4c, 0x5a, 0xea,
0x32, 0xd4, 0xe3, 0x97, 0x4a, 0xe4, 0x00, 0xd0, 0xd8, 0x36, 0xc2, 0x0e, 0xe4, 0xa2, 0x7c, 0x6c }};
0xd0, 0x98, 0x9d, 0x83, 0x0e, 0x03, 0xe1, 0x4e, 0xf6, 0xaf, 0x71, 0xa0, 0xa1, 0xfc, 0x88, 0x38, 0xac, 0xfc, 0xd8, 0x95, 0x06, 0x54, 0x9f, 0x3e, 0xdb, 0xb0, 0xf5, 0x3a, 0xc9, 0x0e, 0x47, 0x90,
}};
AlignedBuffer<64> root_key_data{{
0xc0, 0xe6, 0x58, 0xd6, 0x01, 0xc1, 0xb4, 0xc2, 0x94, 0xb8, 0xf7, 0xa3, 0xec, 0x3e, 0x81, 0xd6, 0x82, 0xb4, 0x89, 0x5c, 0x6d, 0xbf, 0x5c, 0x6e, 0x20, 0xad, 0x39, 0x8f, 0xf4, 0x8f, 0x43, 0x4f,
0x56, 0x4f, 0xdc, 0x22, 0x33, 0x19, 0xb9, 0xbb, 0x4e, 0xc0, 0xba, 0x84, 0x2d, 0xe3, 0xde, 0xf2, 0x26, 0xe8, 0xf7, 0xa8, 0x8f, 0x82, 0x41, 0xe3, 0x1f, 0x5d, 0xe5, 0x56, 0x3a, 0xf4, 0x5e, 0x3c,
}};
AlignedBuffer<32> root_pub_data{{
0x69, 0x8b, 0x43, 0xbb, 0x54, 0xeb, 0x31, 0x2e, 0x5a, 0x07, 0x3f, 0x59, 0x5f, 0x1a, 0xbf, 0xe3,
0x95, 0xf2, 0x7a, 0x6d, 0x1d, 0x64, 0x5c, 0x4b, 0x10, 0x3f, 0xa2, 0xf5, 0xe6, 0x97, 0x5c, 0x70 }};
0x4a, 0x34, 0x3f, 0x9e, 0xf3, 0xda, 0x3d, 0x80, 0x07, 0xc7, 0x09, 0xf9, 0x2f, 0x72, 0xd3, 0x76, 0x56, 0x5a, 0x4c, 0x13, 0xdf, 0xb8, 0xce, 0xc8, 0x53, 0x77, 0x0a, 0x99, 0xbc, 0x06, 0xa7, 0xc0,
}};
AlignedBuffer<32> hash{{
0x22, 0x41, 0xca, 0x66, 0x21, 0x4c, 0x75, 0x40, 0x65, 0x57, 0x9e, 0x81, 0x8c, 0x70, 0x15, 0x2a,
0x71, 0xb6, 0xc1, 0x67, 0x3f, 0x3b, 0x4b, 0x22, 0x31, 0xed, 0x22, 0x30, 0x2e, 0x2a, 0x23, 0x8e }};
AlignedBuffer<32> derived_key_data{{
0xbd, 0x0c, 0x55, 0x32, 0x62, 0x89, 0x61, 0xea, 0x86, 0x10, 0xd2, 0x27, 0x18, 0x51, 0xc0, 0x5e,
0x0e, 0xb1, 0x5a, 0x45, 0xb7, 0xb6, 0x16, 0xbe, 0x37, 0xba, 0x9a, 0x34, 0x39, 0xc4, 0xd0, 0x07 }};
0x64, 0xad, 0xde, 0x17, 0x69, 0x33, 0x92, 0x25, 0x9c, 0xa3, 0xd7, 0x85, 0xa5, 0x2d, 0x3a, 0xa5, 0xa3, 0x9c, 0xdb, 0x99, 0x57, 0xac, 0x54, 0x14, 0x4f, 0x11, 0xa9, 0x90, 0xa0, 0xca, 0xcb, 0xfe,
}};
AlignedBuffer<64> derived_key_data{{
0x96, 0x02, 0xba, 0x16, 0x87, 0x40, 0xb7, 0xb6, 0xc9, 0x0f, 0x85, 0x7b, 0xdc, 0xa9, 0x13, 0x9d, 0x1b, 0xf5, 0x01, 0x54, 0xd1, 0xd1, 0x8f, 0x75, 0x06, 0x4d, 0x4c, 0xea, 0x33, 0xc4, 0xc6, 0x00,
0xb0, 0xef, 0x29, 0x37, 0x7c, 0xe9, 0x84, 0x43, 0x5a, 0x79, 0xa2, 0x3b, 0xef, 0xcd, 0x1c, 0x43, 0xf1, 0x88, 0xff, 0x50, 0xaf, 0x9c, 0x07, 0x6a, 0xc6, 0x19, 0xfb, 0xcc, 0x5d, 0x48, 0x75, 0x92,
}};
AlignedBuffer<32> derived_pub_data{{
0xa0, 0x72, 0x62, 0x22, 0xd7, 0xc0, 0x91, 0x49, 0xe5, 0xe7, 0x86, 0x0d, 0xc1, 0x53, 0x14, 0x02,
0xe9, 0x96, 0xb8, 0xd8, 0x93, 0xb9, 0x2f, 0xe9, 0xc8, 0xf6, 0xf0, 0x5d, 0xe2, 0x30, 0x06, 0x48 }};
0x13, 0xa6, 0x61, 0x5b, 0x78, 0x64, 0x03, 0xd4, 0x8a, 0x88, 0xaa, 0x0d, 0x89, 0xdf, 0x08, 0x46, 0xb3, 0x2f, 0xa9, 0xbb, 0xa8, 0xcc, 0xe1, 0xac, 0x4c, 0xae, 0xc9, 0xd2, 0xf1, 0x35, 0xd1, 0x33,
}};
SecretKey root{seed};
ASSERT_EQ(root.toPublic(), PubKey{root_pub_data});
@ -181,13 +184,39 @@ TEST_F(RealCryptographyTest, TestKnownDerivation)
PrivateKey aprime; // a'
ASSERT_TRUE(crypto->derive_subkey_private(aprime, root, 0, &hash));
ASSERT_EQ(aprime, PrivateKey{derived_key_data});
// We use a different signing hash than Tor, so only the private key value (the first 32 bytes)
// will match:
ASSERT_EQ(aprime.ToHex().substr(0, 64), PrivateKey{derived_key_data}.ToHex().substr(0, 64));
PubKey Aprime; // A'
ASSERT_TRUE(crypto->derive_subkey(Aprime, root.toPublic(), 0, &hash));
ASSERT_EQ(Aprime, PubKey{derived_pub_data});
}
TEST_F(RealCryptographyTest, TestRootSigning)
{
auto crypto = CryptoManager::instance();
SecretKey root_key;
crypto->identity_keygen(root_key);
// We have our own reimplementation of sodium's signing function which can work with derived
// private keys (unlike sodium's built-in which requires starting from a seed). This tests that
// signing using either path produces an identical signature.
const std::string nibbs = "Nibbler";
llarp_buffer_t nibbs_buf{nibbs.data(), nibbs.size()};
Signature sig_sodium;
ASSERT_TRUE(crypto->sign(sig_sodium, root_key, nibbs_buf));
PrivateKey root_privkey;
ASSERT_TRUE(root_key.toPrivate(root_privkey));
Signature sig_ours;
ASSERT_TRUE(crypto->sign(sig_ours, root_privkey, nibbs_buf));
ASSERT_EQ(sig_sodium, sig_ours);
}
TEST_F(RealCryptographyTest, TestGenerateDeriveKey)
{
auto crypto = CryptoManager::instance();
@ -221,6 +250,21 @@ TEST_F(RealCryptographyTest, TestGenerateDeriveKey)
ASSERT_TRUE(aprime.toPublic(Aprime_alt));
ASSERT_EQ(Aprime, Aprime_alt);
// Generate using the same constant and make sure we get an identical privkey (including the
// signing hash value)
PrivateKey aprime_repeat;
ASSERT_TRUE(crypto->derive_subkey_private(aprime_repeat, root_key, 1));
ASSERT_EQ(aprime_repeat, aprime);
// Generate another using a different constant and make sure we get something different
PrivateKey a2;
PubKey A2;
ASSERT_TRUE(crypto->derive_subkey_private(a2, root_key, 2));
ASSERT_TRUE(crypto->derive_subkey(A2, A, 2));
ASSERT_NE(A2, Aprime);
ASSERT_NE(a2.ToHex().substr(0, 64), aprime.ToHex().substr(0, 64));
ASSERT_NE(a2.ToHex().substr(64), aprime.ToHex().substr(64)); // The hash should be different too
}
TEST_F(RealCryptographyTest, TestSignUsingDerivedKey)