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

Conversation

randombit
Copy link
Owner

No description provided.

@coveralls
Copy link

Coverage Status

coverage: 91.074% (+0.001%) from 91.073%
when pulling af3b206 on jack/spake2
into 162a389 on master.

@reneme reneme self-requested a review November 19, 2024 11:42
Copy link
Collaborator

@reneme reneme left a comment

Choose a reason for hiding this comment

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

I have a few suggestions re the API and some nits. Most of them, I prototyped in another branch, including an example.cpp.

Comment on lines +22 to +33
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;
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);
}

}

// 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);

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());

Comment on lines +131 to +145
const EC_AffinePoint& spake2_our_pt(SPAKE2_PeerId whoami) const {
if(whoami == SPAKE2_PeerId::PeerA) {
return m_params.first; // M
} else {
return m_params.second; // N
}
}

const EC_AffinePoint& spake2_their_pt(SPAKE2_PeerId whoami) const {
if(whoami == SPAKE2_PeerId::PeerA) {
return m_params.second; // N
} else {
return m_params.first; // M
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sometimes I do like the ternary:

Suggested change
const EC_AffinePoint& spake2_our_pt(SPAKE2_PeerId whoami) const {
if(whoami == SPAKE2_PeerId::PeerA) {
return m_params.first; // M
} else {
return m_params.second; // N
}
}
const EC_AffinePoint& spake2_their_pt(SPAKE2_PeerId whoami) const {
if(whoami == SPAKE2_PeerId::PeerA) {
return m_params.second; // N
} else {
return m_params.first; // M
}
}
const EC_AffinePoint& spake2_our_pt(SPAKE2_PeerId whoami) const {
return (whoami == SPAKE2_PeerId::PeerA) ? spake2_m() : spake2_n();
}
const EC_AffinePoint& spake2_their_pt(SPAKE2_PeerId whoami) const {
return (whoami == SPAKE2_PeerId::PeerA) ? spake2_n() : spake2_m();
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

On a second note: there's actually no reason to expose those into the public API. Let's just implement them as free-standing functions in spake2.cpp's anonymous namespace.

Comment on lines +178 to +185
// 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);
}
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);

Comment on lines +86 to +90
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);
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"));

Comment on lines +175 to +197
class BOTAN_PUBLIC_API(3, 7) SPAKE2_Context final {
public:
SPAKE2_Context(SPAKE2_PeerId whoami, const SPAKE2_Parameters& params, RandomNumberGenerator& rng) :
m_rng(rng), m_whoami(whoami), m_params(params) {}

/**
* Generate a message for the peer. This can be called only once.
*/
std::vector<uint8_t> generate_message();

/**
* Consume the message from the peer and return the shared secret.
*
* The context should not be used anymore after this point
*/
secure_vector<uint8_t> process_message(std::span<const uint8_t> peer_message);

private:
RandomNumberGenerator& m_rng;
SPAKE2_PeerId m_whoami;
SPAKE2_Parameters m_params;
std::optional<std::pair<std::vector<uint8_t>, EC_Scalar>> m_our_message;
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of keeping the state visible to the user in a context object, we could make generate_message() return an opaque (non-copyable) state object that process_message() requires to be moved into as a parameter. This introduces a bit of boiler plate, but effectively avoids misuse.

Eg.

      struct Internal;

      struct State {
            State(std::unique_ptr<Internal> i);

            ~State();
            State(const State&) = delete;
            State& operator=(const State&) = delete;
            State(State&&) noexcept;
            State& operator=(State&&) noexcept;

            std::unique_ptr<Internal> internal;  // NOLINT(misc-non-private-member-*)
      };

Internal would be implemented in the *.cpp and essentially contain the content of m_our_message.
In full consequence, this could reduce the API to two free-standing functions and remove the need for the context class entirely. I prototyped this in the branch I mentioned in the PR-submission. The example-code in this branch also uses the free-standing functions for illustration.

* Here the shared secret a random scalar. It should have been generated
* using a memory hard function such as Argon2id.
*
* # ⚠️ Warning
Copy link
Collaborator

Choose a reason for hiding this comment

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

I appreciate the UTF-8 emoji, but shouldn't we go for @-warning (sans the -, just to not annoy the GitHub account "warning" with an accidental mention) to let this render properly in Doxygen?

Comment on lines +89 to +91
* # ⚠️ Warning
*
* This interface exists primarily for testing, and is not safe for general use.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does it, though? One might argue, that this constructor provides the flexibility to use a different memory-hard hash function than the default (argon2id). Perhaps we should instead add another constructor that takes a std::span<> w_bytes as an already expanded shared secret, that is then just turned into a scalar of the group.

This would be a major footgun, though, because people might just accidentally pass the password (meant for the std::string_view overload) into this proposed constructor and get no pwhash at all.

}

return result;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps a few negative tests? E.g. when the context string is passed inconsistently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants