Skip to content

Commit 0adba18

Browse files
authored
Merge pull request oxen-io#38 from jagerman/subaccounts
Subaccounts & multi-subscribes
2 parents 5da2d5c + 4b2c281 commit 0adba18

14 files changed

+395
-242
lines changed

spns/bytes.hpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,30 @@ struct AccountID : bytes<33> {};
5151
struct Ed25519PK : bytes<32> {};
5252
struct X25519PK : bytes<32> {};
5353
struct X25519SK : bytes<32> {};
54-
struct SubkeyTag : bytes<32> {};
54+
struct SubaccountTag : bytes<36> {};
5555
struct Signature : bytes<64> {};
5656
struct EncKey : bytes<32> {};
5757

5858
struct Blake2B_32 : bytes<32> {};
5959

60+
struct Subaccount {
61+
SubaccountTag tag; /// Provided by the account owner
62+
Signature sig; /// signature of tag, signed by account owner
63+
64+
// Returns true if two subaccounts have the same tag
65+
bool is_same(const Subaccount& other) const { return tag == other.tag; }
66+
67+
// Returns true if two optional subaccounts refer to the same subaccount or account (i.e. both
68+
// empty, or both set to the same subaccount tag). Does not require identical signatures.
69+
static bool is_same(const std::optional<Subaccount>& a, const std::optional<Subaccount>& b) {
70+
if (a.has_value() != b.has_value())
71+
return false; // One set, one unset
72+
if (!a.has_value())
73+
return false; // Both unset
74+
return a->is_same(*b);
75+
}
76+
};
77+
6078
template <typename T, std::enable_if_t<is_bytes<T>, int> = 0>
6179
inline std::basic_string_view<std::byte> as_bsv(const T& v) {
6280
return {reinterpret_cast<const std::byte*>(v.data()), T::SIZE};

spns/hive/signature.cpp

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,53 @@
99

1010
namespace spns::hive {
1111

12-
void verify_signature(std::string_view sig_msg, const Signature& sig, const Ed25519PK& pubkey) {
12+
void verify_signature(std::string_view sig_msg, const Signature& sig, const Ed25519PK& pubkey, std::string_view descr) {
1313
if (0 != crypto_sign_verify_detached(sig, as_usv(sig_msg).data(), sig_msg.size(), pubkey))
14-
throw signature_verify_failure{"Signature verification failed"};
14+
throw signature_verify_failure{std::string{descr} + " verification failed"};
15+
}
16+
17+
namespace {
18+
constexpr std::byte SUBACC_FLAG_READ{0b0001};
19+
constexpr std::byte SUBACC_FLAG_ANY_PREFIX{0b1000};
1520
}
1621

1722
/// Throws signature_verify_failure on signature failure.
1823
void verify_storage_signature(
1924
std::string_view sig_msg,
2025
const Signature& sig,
21-
const Ed25519PK& pubkey,
22-
const std::optional<SubkeyTag>& subkey_tag) {
26+
const SwarmPubkey& pubkey,
27+
const std::optional<Subaccount>& subaccount) {
28+
29+
if (subaccount) {
30+
// Parse the subaccount tag:
31+
// prefix aka netid (05 for session ids, 03 for groups):
32+
auto prefix = subaccount->tag[0];
33+
// read/write/etc. flags:
34+
auto flags = subaccount->tag[1];
35+
36+
// If you don't have the read bit we can't help you:
37+
if ((flags & SUBACC_FLAG_READ) == std::byte{0})
38+
throw signature_verify_failure{"Invalid subaccount: this subaccount does not have read permission"};
2339

24-
if (subkey_tag) {
25-
// H(c || A, key="OxenSSSubkey")
26-
auto verify_pubkey = blake2b_keyed<Ed25519PK>(subkey_tag_hash_key, *subkey_tag, pubkey);
40+
// Unless the subaccount has the "any prefix" flag, check that the prefix matches the
41+
// account prefix:
42+
if ((flags & SUBACC_FLAG_ANY_PREFIX) == std::byte{0} &&
43+
prefix != pubkey.id[0])
44+
throw signature_verify_failure{"Invalid subaccount: subaccount and main account have mismatched network prefix"};
2745

28-
// c + H(...)
29-
crypto_core_ed25519_scalar_add(verify_pubkey, *subkey_tag, verify_pubkey);
46+
// Verify that the main account has signed the subaccount tag:
47+
verify_signature(subaccount->tag.sv(), subaccount->sig, pubkey.ed25519, "Subaccount auth signature");
3048

31-
// (c + H(...)) A
32-
if (0 != crypto_scalarmult_ed25519_noclamp(verify_pubkey, verify_pubkey, pubkey))
33-
throw signature_verify_failure{"Failed to compute subkey: scalarmult failed"};
49+
// the subaccount pubkey (starts at [4]; [2] and [3] are future use/null padding):
50+
Ed25519PK sub_pk;
51+
std::memcpy(sub_pk.data(), &subaccount->tag[4], 32);
3452

35-
verify_signature(sig_msg, sig, verify_pubkey);
53+
// Verify that the subaccount pubkey signed this message (and thus is allowed, transitively,
54+
// since the main account signed the subaccount):
55+
verify_signature(sig_msg, sig, sub_pk, "Subaccount main signature");
3656

3757
} else {
38-
verify_signature(sig_msg, sig, pubkey);
58+
verify_signature(sig_msg, sig, pubkey.ed25519);
3959
}
4060
}
4161

spns/hive/signature.hpp

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "../bytes.hpp"
66
#include "../utils.hpp"
7+
#include "../swarmpubkey.hpp"
78

89
namespace spns::hive {
910

@@ -14,20 +15,18 @@ class signature_verify_failure : public std::runtime_error {
1415
};
1516

1617
/// Verifies that the given signature is a valid signature for `sig_msg`. Supports regular
17-
/// ed25519_pubkey signatures as well as oxen-storage-server derived subkey signatures (if
18-
/// `subkey_tag` is given).
19-
20-
inline constexpr auto subkey_tag_hash_key = "OxenSSSubkey"sv;
18+
/// ed25519_pubkey signatures as well as oxen-storage-server delegated subaccount signatures (if
19+
/// `subaccount` is given).
2120

2221
// Plain jane Ed25519 signature verification. Throws a `signature_verify_failure` on verification
2322
// failure.
24-
void verify_signature(std::string_view sig_msg, const Signature& sig, const Ed25519PK& pubkey);
23+
void verify_signature(std::string_view sig_msg, const Signature& sig, const Ed25519PK& pubkey, std::string_view descr = "Signature"sv);
2524

2625
/// Throws signature_verify_failure on signature failure.
2726
void verify_storage_signature(
2827
std::string_view sig_msg,
2928
const Signature& sig,
30-
const Ed25519PK& pubkey,
31-
const std::optional<SubkeyTag>& subkey_tag);
29+
const SwarmPubkey& pubkey,
30+
const std::optional<Subaccount>& subaccount);
3231

3332
} // namespace spns::hive

spns/hive/snode.cpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ void SNode::check_subs(
247247
+ 3 + 36 // 1:p and 33:... (also covers 1:P and 32:...)
248248
+ 3 + 2 // 1:n and the le of the l...e list
249249
+ 3 + 3 // 1:d and i1e (only if want_data)
250-
+ 3 + 35 // 1:S and 32:... (only if subkey)
250+
+ 3 + 67 + 3 + 39 // 1:S, 64:..., 1:T, 36:... (for subaccount auth)
251251
;
252252

253253
// The biggest int expression we have is i-32768e; this is almost certainly overkill
@@ -263,15 +263,17 @@ void SNode::check_subs(
263263

264264
// keys in ascii-sorted order!
265265
if (acct.session_ed)
266-
dict.append("P", as_sv(acct.ed25519));
267-
if (sub.subkey_tag)
268-
dict.append("S", as_sv(*sub.subkey_tag));
266+
dict.append("P", acct.ed25519.sv());
267+
if (sub.subaccount) {
268+
dict.append("S", sub.subaccount->sig.sv());
269+
dict.append("T", sub.subaccount->tag.sv());
270+
}
269271
if (sub.want_data)
270272
dict.append("d", 1);
271273
dict.append_list("n").append(sub.namespaces.begin(), sub.namespaces.end());
272274
if (!acct.session_ed)
273-
dict.append("p", as_sv(acct.id));
274-
dict.append("s", as_sv(sub.sig));
275+
dict.append("p", acct.id.sv());
276+
dict.append("s", sub.sig.sv());
275277
dict.append("t", sub.sig_ts);
276278

277279
// Resize away any extra buffer space we didn't fill

spns/hive/subscription.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ static void append_int(std::string& s, Int val) {
2929

3030
Subscription::Subscription(
3131
const SwarmPubkey& pubkey,
32-
std::optional<SubkeyTag> subkey_tag_,
32+
std::optional<Subaccount> subaccount_,
3333
std::vector<int16_t> namespaces_,
3434
bool want_data_,
3535
int64_t sig_ts_,
3636
Signature sig_,
3737
bool _skip_validation) :
3838

39-
subkey_tag{std::move(subkey_tag_)},
39+
subaccount{std::move(subaccount_)},
4040
namespaces{std::move(namespaces_)},
4141
want_data{want_data_},
4242
sig_ts{sig_ts_},
@@ -74,12 +74,12 @@ Subscription::Subscription(
7474
sig_msg += ',';
7575
append_int(sig_msg, namespaces[i]);
7676
}
77-
verify_storage_signature(sig_msg, sig, pubkey.ed25519, subkey_tag);
77+
verify_storage_signature(sig_msg, sig, pubkey, subaccount);
7878
}
7979
}
8080

8181
bool Subscription::covers(const Subscription& other) const {
82-
if (subkey_tag != other.subkey_tag)
82+
if (!Subaccount::is_same(subaccount, other.subaccount))
8383
return false;
8484
if (other.want_data && !want_data)
8585
return false;

spns/hive/subscription.hpp

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,40 +42,41 @@ class subscribe_error : public std::runtime_error {
4242
struct Subscription {
4343
static constexpr std::chrono::seconds SIGNATURE_EXPIRY{14 * 24h};
4444

45-
std::optional<SubkeyTag> subkey_tag;
45+
std::optional<Subaccount> subaccount;
4646
std::vector<int16_t> namespaces;
4747
bool want_data;
4848
int64_t sig_ts;
4949
Signature sig;
5050

5151
Subscription(
5252
const SwarmPubkey& pubkey_,
53-
std::optional<SubkeyTag> subkey_tag_,
53+
std::optional<Subaccount> subaccout_,
5454
std::vector<int16_t> namespaces_,
5555
bool want_data_,
5656
int64_t sig_ts_,
5757
Signature sig_,
5858
bool _skip_validation = false);
5959

6060
// Returns true if `this` and `other` represent the same subscription as far as upstream swarm
61-
// subscription is concerned. That is: same subkey, same namespaces, and same want_data value.
62-
// The caller is responsible for also ensuring that the subscription applies to the same account
63-
// (i.e. has the same SwarmPubkey).
61+
// subscription is concerned. That is: same subaccount tag, same namespaces, and same want_data
62+
// value. The caller is responsible for also ensuring that the subscription applies to the same
63+
// account (i.e. has the same SwarmPubkey).
6464
bool is_same(const Subscription& other) const {
65-
return is_same(other.subkey_tag, other.namespaces, other.want_data);
65+
return is_same(other.subaccount, other.namespaces, other.want_data);
6666
}
6767
// Same as above, but takes the constituent parts.
6868
bool is_same(
69-
const std::optional<SubkeyTag>& o_subkey_tag,
69+
const std::optional<Subaccount>& o_subaccount,
7070
const std::vector<int16_t>& o_namespaces,
7171
bool o_want_data) const {
72-
return subkey_tag == o_subkey_tag && namespaces == o_namespaces && want_data == o_want_data;
72+
return Subaccount::is_same(subaccount, o_subaccount) && namespaces == o_namespaces &&
73+
want_data == o_want_data;
7374
}
7475

7576
// Returns true if `this` subscribes to at least everything needed for `other`; `this` can
7677
// return extra things (e.g. extra namespaces), but cannot omit anything that `other` needs to
77-
// send notifications, nor can the two subscriptions use different subkey tags. This is *only*
78-
// valid for two Subscriptions referring to the same account!
78+
// send notifications, nor can the two subscriptions use different subaccount tags. This is
79+
// *only* valid for two Subscriptions referring to the same account!
7980
bool covers(const Subscription& other) const;
8081

8182
bool is_expired(int64_t now) const { return sig_ts < now - SIGNATURE_EXPIRY.count(); }

0 commit comments

Comments
 (0)