Skip to content
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
4 changes: 3 additions & 1 deletion include/session/session_protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ typedef enum SESSION_PROTOCOL_PRO_STATUS { // See session::ProStatus
SESSION_PROTOCOL_PRO_STATUS_INVALID_USER_SIG,
SESSION_PROTOCOL_PRO_STATUS_VALID,
SESSION_PROTOCOL_PRO_STATUS_EXPIRED,
// Proof carried a version this client doesn't understand; it could not be verified and the
// message is treated as non-pro (see session::ProProofVersion).
SESSION_PROTOCOL_PRO_STATUS_UNSUPPORTED_VERSION,
} SESSION_PROTOCOL_PRO_STATUS;

typedef struct session_protocol_pro_signed_message {
Expand All @@ -70,7 +73,6 @@ typedef struct session_protocol_pro_signed_message {
} session_protocol_pro_signed_message;

typedef struct session_protocol_pro_proof {
uint8_t version;
cbytes32 revocation_tag;
cbytes32 rotating_pubkey;
int64_t expiry_ts;
Expand Down
29 changes: 20 additions & 9 deletions include/session/session_protocol.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ inline constexpr int STANDARD_PINNED_CONVERSATION_LIMIT = 5;
/// envelope.
inline constexpr int COMMUNITY_OR_1O1_MSG_PADDING = 160;

enum ProProofVersion { ProProofVersion_v0 };
/// The Session Pro proof wire-format version. This is a *selector*: it says which ProProof_vN
/// layout a serialized proof uses; it is deliberately NOT stored as a member of the proof struct
/// (see ProProof_v0 / the ProProof alias below). Only v0 exists today.
enum class ProProofVersion : std::uint8_t { v0 = 0 };

/// Rotation window for the Session Pro rotating key: ProProof::rotating_seed yields the same seed
/// for all timestamps within one such period and a fresh one at each boundary.
Expand Down Expand Up @@ -99,13 +102,13 @@ enum class ProStatus {
InvalidUserSig = SESSION_PROTOCOL_PRO_STATUS_INVALID_USER_SIG,
Valid = SESSION_PROTOCOL_PRO_STATUS_VALID, // Proof is verified; has not expired
Expired = SESSION_PROTOCOL_PRO_STATUS_EXPIRED, // Proof is verified; has expired
// Proof's wire version is not one we understand, so it can't be verified; the message is
// delivered as non-pro rather than dropped (see ProProofVersion).
UnsupportedVersion = SESSION_PROTOCOL_PRO_STATUS_UNSUPPORTED_VERSION,
};

class ProProof {
class ProProof_v0 {
public:
/// Version of the proof set by the Session Pro Backend
std::uint8_t version;

/// Opaque revocation tag identifying this proof (from the Session Pro backend)
b32 revocation_tag;

Expand Down Expand Up @@ -226,13 +229,21 @@ class ProProof {
static cleared_b32 rotating_seed(
std::span<const std::byte> master_seed, std::chrono::sys_seconds now);

bool operator==(const ProProof& other) const {
return version == other.version && revocation_tag == other.revocation_tag &&
rotating_pubkey == other.rotating_pubkey && expiry_at == other.expiry_at &&
sig == other.sig;
bool operator==(const ProProof_v0& other) const {
return revocation_tag == other.revocation_tag && rotating_pubkey == other.rotating_pubkey &&
expiry_at == other.expiry_at && sig == other.sig;
}
};

/// The Session Pro proof layout this codebase currently speaks. The wire `version`
/// (ProProofVersion) selects *which* ProProof_vN a serialized proof is, rather than being a field
/// of the proof: today only v0 exists, so this alias points at it. A future format adds its own
/// struct -- `struct ProProof_v1 : ProProof_v0 { ... }` to extend, or a fresh `struct ProProof_v2 {
/// ... }` to replace
/// -- at which point this alias becomes a std::variant (or a virtual base) over the supported
/// versions, chosen from the wire version at parse time.
using ProProof = ProProof_v0;

enum class ProFeaturesForMsgStatus {
Success = SESSION_PROTOCOL_PRO_FEATURES_FOR_MSG_STATUS_SUCCESS,

Expand Down
7 changes: 3 additions & 4 deletions src/config/pro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ bool ProConfig::load(std::string_view bt_encoded) {
auto seed = d.require_span<std::byte, crypto_sign_ed25519_SEEDBYTES>("r");
auto sig = d.require_span<std::byte, sizeof(proof.sig)>("s");

// The config proof format is v0 by definition (a future format takes a new key, not an
// in-dict version marker -- an opaque value can't carry a version that describes itself
// across a per-key merge).
proof.version = ProProofVersion_v0;
// The config proof is v0 by definition: a future format would take a new config key, not an
// in-dict version marker (an opaque per-key-merged value can't carry a version describing
// itself), so there is nothing to select here -- load() only ever produces a ProProof_v0.
proof.expiry_at = std::chrono::sys_seconds{std::chrono::seconds{expiry}};
std::memcpy(proof.revocation_tag.data(), tag.data(), proof.revocation_tag.size());
std::memcpy(proof.sig.data(), sig.data(), proof.sig.size());
Expand Down
2 changes: 0 additions & 2 deletions src/config/user_profile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,6 @@ LIBSESSION_C_API bool user_profile_get_pro_config(const config_object* conf, pro
static_assert(sizeof pro->proof.revocation_tag == sizeof(val->proof.revocation_tag));
static_assert(sizeof pro->proof.rotating_pubkey == sizeof(val->proof.rotating_pubkey));
static_assert(sizeof pro->proof.sig == sizeof(val->proof.sig));
pro->proof.version = val->proof.version;
std::memcpy(
pro->proof.revocation_tag.data,
val->proof.revocation_tag.data(),
Expand All @@ -541,7 +540,6 @@ LIBSESSION_C_API bool user_profile_get_pro_config(const config_object* conf, pro

LIBSESSION_C_API void user_profile_set_pro_config(config_object* conf, const pro_pro_config* pro) {
ProConfig val = {};
val.proof.version = pro->proof.version;
std::memcpy(
val.proof.revocation_tag.data(),
pro->proof.revocation_tag.data,
Expand Down
6 changes: 6 additions & 0 deletions src/json_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <session/types.hpp>
#include <span>
#include <string_view>
#include <type_traits>

namespace session::json {

Expand All @@ -38,6 +39,11 @@ std::pair<bool, std::string_view> is(const nlohmann::json& v) {
else if constexpr (std::integral<T>)
// is_number_integer() (not is_number()) so a fractional value is rejected, not truncated.
return {v.is_number_integer(), "an integer"};
else if constexpr (std::is_enum_v<T>)
// A (scoped) enum reads as its underlying integer -- nlohmann's default serializer converts
// through the underlying type (extract's get_to) -- so callers can request the enum
// directly, e.g. require<ProProofVersion>(obj, "version").
return {v.is_number_integer(), "an integer"};
else if constexpr (is_one_of<T, std::string, std::string_view>)
return {v.is_string(), "a string"};
else if constexpr (std::same_as<T, nlohmann::json::array_t>)
Expand Down
6 changes: 4 additions & 2 deletions src/pro_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ namespace {
// Fills the common proof payload (add-payment and generate-proof both reply with exactly a
// proof) from the already-extracted `result` object.
void fill_proof(const nlohmann::json::object_t& result_obj, GenerateProProofResponse& result) {
result.proof.version = json::require<uint8_t>(result_obj, "version");
// No wire `version` to read: this endpoint returns a ProProof_v0 by construction -- the
// format is fixed by the endpoint we asked, and the proof's version is bound into its
// signature via the personalisation, not carried as a field. A future format is a new
// endpoint returning a new ProProof_vN, not a version bump on this response.
result.proof.expiry_at = json::require<std::chrono::sys_seconds>(result_obj, "expiry_ts");
json::require_binary(result_obj, "revocation_tag", result.proof.revocation_tag);
json::require_binary(result_obj, "rotating_pkey", result.proof.rotating_pubkey);
Expand Down Expand Up @@ -739,7 +742,6 @@ session_pro_backend_pro_proof_response_parse(const char* json, size_t json_len)

// Success and error responses fold into one path -- different fields populated.
const auto& p = owned->proof;
result.proof.version = p.version;
result.proof.expiry_ts = session::epoch_seconds(p.expiry_at);
std::memcpy(
result.proof.revocation_tag.data, p.revocation_tag.data(), p.revocation_tag.size());
Expand Down
123 changes: 70 additions & 53 deletions src/session_protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ static session_protocol_envelope envelope_from_cpp(const session::Envelope& cpp)
static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedPro& cpp) {
session_protocol_decoded_pro result = {};
result.status = static_cast<SESSION_PROTOCOL_PRO_STATUS>(cpp.status);
result.proof.version = cpp.proof.version;
std::memcpy(
result.proof.revocation_tag.data,
cpp.proof.revocation_tag.data(),
Expand All @@ -109,7 +108,6 @@ static session_protocol_decoded_pro decoded_pro_from_cpp(const session::DecodedP
// decoded_pro_from_cpp).
static session::ProProof proof_from_c(const session_protocol_pro_proof& c) {
session::ProProof proof = {};
proof.version = c.version;
std::memcpy(
proof.revocation_tag.data(), c.revocation_tag.data, proof.revocation_tag.max_size());
std::memcpy(
Expand Down Expand Up @@ -445,19 +443,32 @@ static void parse_envelope_fields(
parse_common_envelope_fields(result.envelope, envelope);
}

// Parses and validates the proof and feature flags embedded in a protobuf ProMessage into a
// DecodedPro. Throws if the proof is missing or malformed. The caller is responsible for evaluating
// the resulting proof's `.status`.
// Parses the proof and feature flags embedded in a protobuf ProMessage into a DecodedPro. A proof
// whose wire `version` we don't recognize is NOT fatal: it degrades to a non-pro message flagged
// ProStatus::UnsupportedVersion (the caller then skips signature evaluation), so a future proof
// format cannot make an older client silently drop the whole message. Throws only when the proof is
// absent, or when a v0 proof is structurally malformed (i.e. corruption, not forward-compat). The
// caller evaluates `.status` for a proof that parsed.
static DecodedPro parse_pro_message(const SessionProtos::ProMessage& pro_msg) {
DecodedPro pro = {};
if (!pro_msg.has_proof())
throw std::runtime_error{"Parse failed, pro config missing proof"};

const SessionProtos::ProProof& proto_proof = pro_msg.proof();
pro.msg_flags = static_cast<ProMessageFlags>(pro_msg.msgbitset());
pro.profile_flags = static_cast<ProProfileFlags>(pro_msg.profilebitset());

// The wire `version` selects which proof layout this is; we only understand v0. Any other (or a
// missing) version is a proof we can't verify -- degrade to non-pro rather than throw.
if (!proto_proof.has_version() ||
static_cast<ProProofVersion>(proto_proof.version()) != ProProofVersion::v0) {
pro.status = ProStatus::UnsupportedVersion;
return pro;
}

// A v0 proof with the wrong shape is corruption, not forward-compat: hard error.
ProProof& proof = pro.proof;
bool valid = proto_proof.has_version() &&
proto_proof.version() == static_cast<std::uint32_t>(ProProofVersion_v0) &&
proto_proof.has_revocationtag() &&
bool valid = proto_proof.has_revocationtag() &&
proto_proof.revocationtag().size() == proof.revocation_tag.max_size() &&
proto_proof.has_rotatingpublickey() &&
proto_proof.rotatingpublickey().size() == proof.rotating_pubkey.max_size() &&
Expand All @@ -466,8 +477,6 @@ static DecodedPro parse_pro_message(const SessionProtos::ProMessage& pro_msg) {
if (!valid)
throw std::runtime_error{"Parse failed, pro metadata was malformed"};

pro.msg_flags = static_cast<ProMessageFlags>(pro_msg.msgbitset());
pro.profile_flags = static_cast<ProProfileFlags>(pro_msg.profilebitset());
std::memcpy(
proof.revocation_tag.data(),
proto_proof.revocationtag().data(),
Expand Down Expand Up @@ -529,19 +538,23 @@ static void parse_content_and_pro(
result.envelope.flags |= SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG;
DecodedPro& pro = result.pro.emplace(parse_pro_message(content.promessage()));

// Evaluate the pro status given the extracted components (was it signed, is it expired,
// was the message signed validly?)
// Note that we sign the envelope content wholesale. For 1o1 which are padded to 160
// bytes, this means that we expected the user to have signed the padding as well.
auto unix_ts = std::chrono::floor<std::chrono::seconds>(
std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::milliseconds(content.sigtimestamp())));
// pro_sig.size() validated == 64 above
pro.status = pro.proof.status(
pro_backend_pubkey,
unix_ts,
to_byte_span<64>(pro_sig.data()),
to_span(envelope.content()));
// A proof we couldn't parse (unknown version) is already flagged
// ProStatus::UnsupportedVersion, with no v0 proof to check -- leave it as non-pro.
if (pro.status != ProStatus::UnsupportedVersion) {
// Evaluate the pro status given the extracted components (was it signed, is it
// expired, was the message signed validly?)
// Note that we sign the envelope content wholesale. For 1o1 which are padded to 160
// bytes, this means that we expected the user to have signed the padding as well.
auto unix_ts = std::chrono::floor<std::chrono::seconds>(
std::chrono::sys_time<std::chrono::milliseconds>(
std::chrono::milliseconds(content.sigtimestamp())));
// pro_sig.size() validated == 64 above
pro.status = pro.proof.status(
pro_backend_pubkey,
unix_ts,
to_byte_span<64>(pro_sig.data()),
to_span(envelope.content()));
}
}
}
}
Expand Down Expand Up @@ -725,36 +738,40 @@ DecodedCommunityMessage decode_for_community(
if (result.pro_sig && content.has_promessage()) {
DecodedPro& pro = result.pro.emplace(parse_pro_message(content.promessage()));

// Evaluate the pro status given the extracted components (was it signed, is it expired,
// was the message signed validly?)
//
// IMPORTANT: We have to bit-manipulate the content because we're including the signature
// inside the payload itself that we had to sign. But we originally signed the payload
// without a signature set in it. This is only the case if we're dealing with a `Content`
// message that had the signature inside the content instead of the envelope.
if (result.envelope) {
// Entering the `pro_sig` and `result.envelope` branch means that the envelope must have
// a pro signature.
assert(result.envelope->flags & SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG);
pro.status = pro.proof.status(
pro_backend_pubkey, unix_ts, *result.pro_sig, result.content_plaintext);
} else {
SessionProtos::Content content_copy_without_sig = content;
assert(content_copy_without_sig.has_prosigforcommunitymessageonly());

// Remove signature from the payload
content_copy_without_sig.clear_prosigforcommunitymessageonly();
assert(!content_copy_without_sig.has_prosigforcommunitymessageonly());

// Reserialise the payload without the signature, repad it then verify the signature
std::vector<std::byte> content_copy_without_sig_payload =
pad_message(to_span(content_copy_without_sig.SerializeAsString()));

pro.status = pro.proof.status(
pro_backend_pubkey,
unix_ts,
*result.pro_sig,
to_span(content_copy_without_sig_payload));
// A proof we couldn't parse (unknown version) is already flagged
// ProStatus::UnsupportedVersion, with no v0 proof to check -- leave it as non-pro.
if (pro.status != ProStatus::UnsupportedVersion) {
// Evaluate the pro status given the extracted components (was it signed, is it expired,
// was the message signed validly?)
//
// IMPORTANT: We have to bit-manipulate the content because we're including the
// signature inside the payload itself that we had to sign. But we originally signed the
// payload without a signature set in it. This is only the case if we're dealing with a
// `Content` message that had the signature inside the content instead of the envelope.
if (result.envelope) {
// Entering the `pro_sig` and `result.envelope` branch means that the envelope must
// have a pro signature.
assert(result.envelope->flags & SESSION_PROTOCOL_ENVELOPE_FLAGS_PRO_SIG);
pro.status = pro.proof.status(
pro_backend_pubkey, unix_ts, *result.pro_sig, result.content_plaintext);
} else {
SessionProtos::Content content_copy_without_sig = content;
assert(content_copy_without_sig.has_prosigforcommunitymessageonly());

// Remove signature from the payload
content_copy_without_sig.clear_prosigforcommunitymessageonly();
assert(!content_copy_without_sig.has_prosigforcommunitymessageonly());

// Reserialise the payload without the signature, repad it then verify the signature
std::vector<std::byte> content_copy_without_sig_payload =
pad_message(to_span(content_copy_without_sig.SerializeAsString()));

pro.status = pro.proof.status(
pro_backend_pubkey,
unix_ts,
*result.pro_sig,
to_span(content_copy_without_sig_payload));
}
}
}

Expand Down
4 changes: 0 additions & 4 deletions tests/test_config_pro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ TEST_CASE("Pro", "[config][pro]") {
{
// CPP
pro_cpp.rotating_privkey = rotating_sk;
// Config never persists the proof version (see ProConfig::load); a loaded proof is v0.
pro_cpp.proof.version = session::ProProofVersion_v0;
pro_cpp.proof.rotating_pubkey = rotating_pk;
pro_cpp.proof.expiry_at = std::chrono::sys_seconds(1s);
constexpr auto revocation_tag =
Expand All @@ -34,7 +32,6 @@ TEST_CASE("Pro", "[config][pro]") {

// C
std::memcpy(pro.rotating_privkey.data, rotating_sk.data(), rotating_sk.size());
pro.proof.version = pro_cpp.proof.version;
std::memcpy(pro.proof.rotating_pubkey.data, rotating_pk.data(), rotating_pk.size());
pro.proof.expiry_ts = pro_cpp.proof.expiry_at.time_since_epoch().count();
std::memcpy(pro.proof.revocation_tag.data, revocation_tag.data(), revocation_tag.size());
Expand Down Expand Up @@ -80,7 +77,6 @@ TEST_CASE("Pro", "[config][pro]") {
session::config::ProConfig loaded_pro = {};
CHECK(loaded_pro.load(encoded));
CHECK(loaded_pro.rotating_privkey == pro_cpp.rotating_privkey);
CHECK(loaded_pro.proof.version == session::ProProofVersion_v0); // never persisted
CHECK(loaded_pro.proof.revocation_tag == pro_cpp.proof.revocation_tag);
CHECK(loaded_pro.proof.rotating_pubkey ==
pro_cpp.proof.rotating_pubkey); // derived from seed
Expand Down
Loading