Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SPAKE2 password authenticated key exchange (RFC 9382) #4438

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api_ref/contents.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ API Reference
keywrap
passhash
cryptobox
spake2
srp
psk_db
filters
Expand Down
80 changes: 80 additions & 0 deletions doc/api_ref/spake2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
SPAKE2 Password Authenticated Key Exchange
=============================================
Comment on lines +1 to +2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also mention that new algorithm in the doxygen index page in types.h.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, an example would be nice.


.. versionadded:: 3.7.0

An implementation of SPAKE2 password authenticated key exchange
compatible with RFC 9832 is included.

SPAKE2 requires each peer know its "role" within the protocol, namely being A
or B. This is common in most protocols; for example in a client/server
architecture, the client could be A and the server B.

This implementation of SPAKE2 does not include the key confirmation step. Thus,
on its own, there is no guarantee that the two peers actually share the same
secret key. Normally the SPAKE2 shared secret is subsequently used to encrypt
one or more messages; this serves to confirm the key. It is possible to
implement RFC 9832 compatible key confirmation, as described in RFC 9832 Section 4.

Each instance is configured with a set of parameters

.. cpp:class:: SPAKE2_Parameters

.. cpp:function:: SPAKE2_Parameters(const EC_Group& group, \
std::string_view shared_secret, \
std::span<const uint8_t> a_identity = {}, \
std::span<const uint8_t> b_identity = {}, \
std::span<const uint8_t> context = {}, \
std::string_view hash = "SHA-512", \
bool per_user_params = true)

Constructs a new set of parameters.

The elliptic curve group should typically be P-256, P-384, or P-521.

The ``shared_secret`` is the low entropy user secret. This is hashed using
Argon2id to generate the SPAKE2 ``w`` parameter.

The identities of the two peers are specified in ``a_identity`` and
``b_identity``. These can be left empty if there is no possible identity;
however even the strings "client" and "server" would be preferable rather
than leaving them completely blank.

The ``context`` is some arbitrary bytestring which is included when hashing
the shared secret. It can be left empty, or can be used to identity eg
the protocol in use.

The ``hash_fn`` parameter specifies a hash function to use. Use SHA-512.

If ``per_user_params`` is true, then SPAKE2 will proceed using system
parameters N/M which were generated using RFC 9380 hash to curve using the
identities and context string as inputs. This makes SPAKE2 "quantum
annoying"; baseline SPAKE2 can be broken by anyone who can recover the
discrete logarithms of the fixed N/M parameters included in the RFC. This
makes life difficult for an attacker who can compute discrete logarithms,
but cannot do so cheaply.

.. cpp:enum-class:: SPAKE2_PeerId

.. cpp:enumerator:: SPAKE2_PeerId::PeerA

.. cpp:enumerator:: SPAKE2_PeerId::PeerB


.. cpp:class:: SPAKE2_Context

.. cpp:function:: SPAKE2_Context(SPAKE2_PeerId whoami, \
const SPAKE2_Parameters& params, \
RandomNumberGenerator& rng)

Prepare for a SPAKE2 exchange

.. cpp:function:: std::vector<uint8_t> generate_message()

Proceed with the protocol. Generate a message, which must be sent
to the peer.

.. cpp:function:: secure_vector<uint8_t> process_message(std::span<const uint8_t> peer_message)

Complete the key exchange, returning the shared secret. Will throw an exception
if an error occurs (eg the peer message is not formatted correctly)
19 changes: 19 additions & 0 deletions src/lib/pake/spake2/info.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<defines>
PAKE_SPAKE2 -> 20240821
</defines>

<module_info>
name -> "SPAKE2"
brief -> "SPAKE2 PAKE"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
brief -> "SPAKE2 PAKE"
brief -> "SPAKE2 Password-Authenticated Key Exchange"

... when I was starting out with crypto, I was always grateful for abbreviation resolution. :)

</module_info>

<header:public>
spake2.h
</header:public>

<requires>
argon2
hkdf
hmac
sha2_64
</requires>
Comment on lines +14 to +19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<requires>
argon2
hkdf
hmac
sha2_64
</requires>
<requires>
argon2
ec_group
hkdf
hmac
sha2_64
</requires>

... missing ec_group for botan/ec_apoint.h and friends.

195 changes: 195 additions & 0 deletions src/lib/pake/spake2/spake2.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* (C) 2024 Jack Lloyd
*
* Botan is released under the Simplified BSD License (see license.txt)
*/

#include <botan/spake2.h>

#include <botan/hash.h>
#include <botan/hex.h>
#include <botan/pwdhash.h>
#include <botan/internal/loadstor.h>
#include <botan/internal/stl_util.h>

namespace Botan {

namespace {

std::vector<uint8_t> format_spake2_ad(std::span<const uint8_t> a_identity,
std::span<const uint8_t> b_identity,
std::span<const uint8_t> context) {
std::vector<uint8_t> ad(a_identity.size() + b_identity.size() + context.size() + 3 * 8);
BufferStuffer stuffer(ad);

auto append_with_le64 = [&](std::span<const uint8_t> data) {
stuffer.append(store_le(static_cast<uint64_t>(data.size())));
stuffer.append(data);
};

append_with_le64(a_identity);
append_with_le64(b_identity);
append_with_le64(context);
return ad;
Comment on lines +22 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

concat() could make this easier:

Suggested change
std::vector<uint8_t> ad(a_identity.size() + b_identity.size() + context.size() + 3 * 8);
BufferStuffer stuffer(ad);
auto append_with_le64 = [&](std::span<const uint8_t> data) {
stuffer.append(store_le(static_cast<uint64_t>(data.size())));
stuffer.append(data);
};
append_with_le64(a_identity);
append_with_le64(b_identity);
append_with_le64(context);
return ad;
auto store_le64 = [](uint64_t s) { return store_le(s); };
// clang-format off
return concat<std::vector<uint8_t>>(store_le64(a_identity.size()), a_identity,
store_le64(b_identity.size()), b_identity,
store_le64(context.size()), context);
// clang-format on

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps pull the store_le64 lambda as a free-standing function into the anonymous namespace and reuse it for the hash calculation in process_message().

auto store_le64(uint64_t n) -> std::array<uint8_t, 8> {
   return store_le(n);
}

}

} // namespace

EC_Scalar SPAKE2_Parameters::hash_shared_secret(const EC_Group& group,
std::string_view shared_secret,
std::span<const uint8_t> a_identity,
std::span<const uint8_t> b_identity,
std::span<const uint8_t> context) {
constexpr size_t M = 128 * 1024;
constexpr size_t t = 3;
constexpr size_t p = 1;

const auto ad = format_spake2_ad(a_identity, b_identity, context);

auto pwhash_fam = PasswordHashFamily::create_or_throw("Argon2id");
auto pwhash = pwhash_fam->from_params(M, t, p);

secure_vector<uint8_t> w_bytes(group.get_order_bytes() + 16);
pwhash->hash(w_bytes, shared_secret, {}, ad, {});
Comment on lines +52 to +53
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
secure_vector<uint8_t> w_bytes(group.get_order_bytes() + 16);
pwhash->hash(w_bytes, shared_secret, {}, ad, {});
// RFC 9382 Section 3.2
// Standards, such as NIST.SP.800-56Ar3, suggest taking mod p of a hash
// value that is 64 bits longer than that needed to represent p to remove
// statistical bias introduced by the modulation.
secure_vector<uint8_t> w_bytes(group.get_order_bytes() + 16);
pwhash->hash(w_bytes, shared_secret, {}, ad, {});

... I'm guessing this is the reason for the magic 16. :)


return EC_Scalar::from_bytes_mod_order(group, w_bytes);
}

SPAKE2_Parameters::SPAKE2_Parameters(const EC_Group& group,
std::string_view shared_secret,
std::span<const uint8_t> a_identity,
std::span<const uint8_t> b_identity,
std::span<const uint8_t> context,
std::string_view hash,
bool per_user_params) :
SPAKE2_Parameters(group,
SPAKE2_Parameters::hash_shared_secret(group, shared_secret, a_identity, b_identity, context),
a_identity,
b_identity,
context,
hash,
per_user_params) {}

namespace {

std::pair<EC_AffinePoint, EC_AffinePoint> spake2_params(const EC_Group& group,
std::string_view hash,
std::span<const uint8_t> a_identity,
std::span<const uint8_t> b_identity,
std::span<const uint8_t> context,
bool per_user_params) {
BOTAN_ARG_CHECK(group.has_cofactor() == false, "SPAKE2 not supported with this curve");

if(per_user_params) {
auto input = format_spake2_ad(a_identity, b_identity, context);

constexpr uint8_t spake2_m[] = {'S', 'P', 'A', 'K', 'E', '2', ' ', 'M'};
constexpr uint8_t spake2_n[] = {'S', 'P', 'A', 'K', 'E', '2', ' ', 'N'};

auto m = EC_AffinePoint::hash_to_curve_ro(group, hash, input, spake2_m);
auto n = EC_AffinePoint::hash_to_curve_ro(group, hash, input, spake2_n);
Comment on lines +86 to +90
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
constexpr uint8_t spake2_m[] = {'S', 'P', 'A', 'K', 'E', '2', ' ', 'M'};
constexpr uint8_t spake2_n[] = {'S', 'P', 'A', 'K', 'E', '2', ' ', 'N'};
auto m = EC_AffinePoint::hash_to_curve_ro(group, hash, input, spake2_m);
auto n = EC_AffinePoint::hash_to_curve_ro(group, hash, input, spake2_n);
auto as_span = [](std::string_view domsep) {
return std::span(cast_char_ptr_to_uint8(domsep.data()), domsep.size());
};
auto m = EC_AffinePoint::hash_to_curve_ro(group, hash, input, as_span("SPAKE M"));
auto n = EC_AffinePoint::hash_to_curve_ro(group, hash, input, as_span("SPAKE N"));


return std::make_pair(m, n);
} else {
const OID& group_id = group.get_curve_oid();

auto decode_pt = [&](std::string_view pt) -> EC_AffinePoint { return EC_AffinePoint(group, hex_decode(pt)); };

if(group_id == OID{1, 2, 840, 10045, 3, 1, 7}) {
auto m = decode_pt("02886e2f97ace46e55ba9dd7242579f2993b64e16ef3dcab95afd497333d8fa12f");
auto n = decode_pt("03d8bbd6c639c62937b04d997f38c3770719c629d7014d49a24b4f98baa1292b49");
return std::make_pair(m, n);
} else if(group_id == OID{1, 3, 132, 0, 34}) {
auto m = decode_pt(
"030ff0895ae5ebf6187080a82d82b42e2765e3b2f8749c7e05eba366434b363d3dc36f15314739074d2eb8613fceec2853");
auto n = decode_pt(
"02c72cf2e390853a1c1c4ad816a62fd15824f56078918f43f922ca21518f9c543bb252c5490214cf9aa3f0baab4b665c10");
return std::make_pair(m, n);
} else if(group_id == OID{1, 3, 132, 0, 35}) {
auto m = decode_pt(
"02003f06f38131b2ba2600791e82488e8d20ab889af753a41806c5db18d37d85608cfae06b82e4a72cd744c719193562a653ea1f119eef9356907edc9b56979962d7aa");
auto n = decode_pt(
"0200c7924b9ec017f3094562894336a53c50167ba8c5963876880542bc669e494b2532d76c5b53dfb349fdf69154b9e0048c58a42e8ed04cef052a3bc349d95575cd25");
return std::make_pair(m, n);
} else {
throw Not_Implemented("There are no defined SPAKE2 parameters for this curve");
}
Comment on lines +98 to +116
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps note the name of the curves for those who don't know the OIDs by heart. 😏

Suggested change
if(group_id == OID{1, 2, 840, 10045, 3, 1, 7}) {
auto m = decode_pt("02886e2f97ace46e55ba9dd7242579f2993b64e16ef3dcab95afd497333d8fa12f");
auto n = decode_pt("03d8bbd6c639c62937b04d997f38c3770719c629d7014d49a24b4f98baa1292b49");
return std::make_pair(m, n);
} else if(group_id == OID{1, 3, 132, 0, 34}) {
auto m = decode_pt(
"030ff0895ae5ebf6187080a82d82b42e2765e3b2f8749c7e05eba366434b363d3dc36f15314739074d2eb8613fceec2853");
auto n = decode_pt(
"02c72cf2e390853a1c1c4ad816a62fd15824f56078918f43f922ca21518f9c543bb252c5490214cf9aa3f0baab4b665c10");
return std::make_pair(m, n);
} else if(group_id == OID{1, 3, 132, 0, 35}) {
auto m = decode_pt(
"02003f06f38131b2ba2600791e82488e8d20ab889af753a41806c5db18d37d85608cfae06b82e4a72cd744c719193562a653ea1f119eef9356907edc9b56979962d7aa");
auto n = decode_pt(
"0200c7924b9ec017f3094562894336a53c50167ba8c5963876880542bc669e494b2532d76c5b53dfb349fdf69154b9e0048c58a42e8ed04cef052a3bc349d95575cd25");
return std::make_pair(m, n);
} else {
throw Not_Implemented("There are no defined SPAKE2 parameters for this curve");
}
if(group_id == OID{1, 2, 840, 10045, 3, 1, 7}) { // secp256r1
auto m = decode_pt("02886e2f97ace46e55ba9dd7242579f2993b64e16ef3dcab95afd497333d8fa12f");
auto n = decode_pt("03d8bbd6c639c62937b04d997f38c3770719c629d7014d49a24b4f98baa1292b49");
return std::make_pair(m, n);
} else if(group_id == OID{1, 3, 132, 0, 34}) { // secp384r1
auto m = decode_pt(
"030ff0895ae5ebf6187080a82d82b42e2765e3b2f8749c7e05eba366434b363d3dc36f15314739074d2eb8613fceec2853");
auto n = decode_pt(
"02c72cf2e390853a1c1c4ad816a62fd15824f56078918f43f922ca21518f9c543bb252c5490214cf9aa3f0baab4b665c10");
return std::make_pair(m, n);
} else if(group_id == OID{1, 3, 132, 0, 35}) { // secp521r1
auto m = decode_pt(
"02003f06f38131b2ba2600791e82488e8d20ab889af753a41806c5db18d37d85608cfae06b82e4a72cd744c719193562a653ea1f119eef9356907edc9b56979962d7aa");
auto n = decode_pt(
"0200c7924b9ec017f3094562894336a53c50167ba8c5963876880542bc669e494b2532d76c5b53dfb349fdf69154b9e0048c58a42e8ed04cef052a3bc349d95575cd25");
return std::make_pair(m, n);
} else {
throw Not_Implemented("There are no defined SPAKE2 parameters for this curve");
}

}
}

} // namespace

SPAKE2_Parameters::SPAKE2_Parameters(const EC_Group& group,
const EC_Scalar& shared_secret,
std::span<const uint8_t> a_identity,
std::span<const uint8_t> b_identity,
std::span<const uint8_t> context,
std::string_view hash,
bool per_user_params) :
m_group(group),
m_params(spake2_params(m_group, hash, a_identity, b_identity, context, per_user_params)),
m_w(shared_secret),
m_hash_fn(hash),
m_a_identity(a_identity.begin(), a_identity.end()),
m_b_identity(b_identity.begin(), b_identity.end()) {}

std::vector<uint8_t> SPAKE2_Context::generate_message() {
BOTAN_STATE_CHECK(!m_our_message.has_value());

const auto eph_key = EC_Scalar::random(m_params.group(), m_rng);

const auto& N_or_M = m_params.spake2_our_pt(m_whoami);
const auto& g = EC_AffinePoint::generator(m_params.group());
// Compute g*x + w*{M,N}
auto msg = EC_AffinePoint::mul_px_qy(g, eph_key, N_or_M, m_params.spake2_w(), m_rng).serialize_uncompressed();

m_our_message = std::make_pair(msg, eph_key);

return msg;
}

secure_vector<uint8_t> SPAKE2_Context::process_message(std::span<const uint8_t> peer_message) {
BOTAN_STATE_CHECK(m_our_message.has_value());

// Reject anything except uncompressed points
if(peer_message.empty() || peer_message[0] != 0x04) {
throw Decoding_Error("SPAKE2 key share was invalid");
}

// Will throw if not on the curve
EC_AffinePoint peer_pt(m_params.group(), peer_message);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
EC_AffinePoint peer_pt(m_params.group(), peer_message);
const EC_AffinePoint peer_pt(m_params.group(), peer_message);


const auto& [our_pt, eph_key] = m_our_message.value();
const auto& N_or_M = m_params.spake2_their_pt(m_whoami);
// Compute x*(pt-w*N_or_M)
const auto neg_xw = eph_key.negate() * m_params.spake2_w();
const auto K = EC_AffinePoint::mul_px_qy(peer_pt, eph_key, N_or_M, neg_xw, m_rng);

auto hash_fn = HashFunction::create_or_throw(m_params.hash_function());

auto append_to_hash_with_le64 = [&](std::span<const uint8_t> data) {
hash_fn->update(store_le(static_cast<uint64_t>(data.size())));
hash_fn->update(data);
};

append_to_hash_with_le64(m_params.a_identity());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
append_to_hash_with_le64(m_params.a_identity());
// Calculate TT
append_to_hash_with_le64(m_params.a_identity());

append_to_hash_with_le64(m_params.b_identity());

// Always pA followed by pB:
if(m_whoami == SPAKE2_PeerId::PeerA) {
append_to_hash_with_le64(our_pt);
append_to_hash_with_le64(peer_message);
} else {
append_to_hash_with_le64(peer_message);
append_to_hash_with_le64(our_pt);
}
Comment on lines +178 to +185
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Along the lines of m_params.spake2_their_pt(), we could add this to the params class:

      auto spake2_sort_messages(SPAKE2_PeerId whoami,
                                std::span<const uint8_t> ours,
                                std::span<const uint8_t> theirs) const
         -> std::pair<std::span<const uint8_t>, std::span<const uint8_t>> {
         if(whoami == SPAKE2_PeerId::PeerB) {
            std::swap(ours, theirs);
         }
         return {ours, theirs};
      }

... and then reduce this code to:

Suggested change
// Always pA followed by pB:
if(m_whoami == SPAKE2_PeerId::PeerA) {
append_to_hash_with_le64(our_pt);
append_to_hash_with_le64(peer_message);
} else {
append_to_hash_with_le64(peer_message);
append_to_hash_with_le64(our_pt);
}
auto [pA, pB] = m_params.spake2_sort_messages(m_whoami, our_pt, peer_message);
append_to_hash_with_le64(pA);
append_to_hash_with_le64(pB);


append_to_hash_with_le64(K.serialize_uncompressed());
append_to_hash_with_le64(m_params.spake2_w().serialize());

m_our_message.reset();

return hash_fn->final();
}

} // namespace Botan
Loading
Loading