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 25-blinding support #213

Open
wants to merge 17 commits into
base: dev
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
22 changes: 18 additions & 4 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,31 @@ def banned_user(db):


@pytest.fixture
def blind_user(db):
def blind15_user(db):
import user

return user.User(blinded=True)
return user.User(blinded15=True)


@pytest.fixture
def blind_user2(db):
def blind15_user2(db):
import user

return user.User(blinded=True)
return user.User(blinded15=True)


@pytest.fixture
def blind25_user(db):
import user

return user.User(blinded25=True)


@pytest.fixture
def blind25_user2(db):
import user

return user.User(blinded25=True)


@pytest.fixture
Expand Down
1 change: 0 additions & 1 deletion contrib/auth-example.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def get_signing_headers(
body,
blinded: bool = True,
):

assert len(server_pk) == 32
assert len(nonce) == 16

Expand Down
64 changes: 64 additions & 0 deletions contrib/blind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python3

import sys
import nacl.bindings as sodium
import nacl.hash
import nacl.signing
from nacl.encoding import RawEncoder
from pyonionreq import xed25519

if len(sys.argv) < 3:
print(
f"Usage: {sys.argv[0]} SERVERPUBKEY {{SESSIONID|\"RANDOM\"}} [SESSIONID ...] -- blinds IDs",
file=sys.stderr,
)
sys.exit(1)

server_pk = sys.argv[1]
sids = sys.argv[2:]

if len(server_pk) != 64 or not all(c in '0123456789ABCDEFabcdef' for c in server_pk):
print(f"Invalid argument: expected 64 hex digit server pk as first argument")
sys.exit(2)

server_pk = bytes.fromhex(server_pk)

print(nacl.hash.blake2b(server_pk, digest_size=64, encoder=RawEncoder))

k15 = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(server_pk, digest_size=64, encoder=RawEncoder)
)


for i in range(len(sids)):
if sids[i] == "RANDOM":
sids[i] = (
"05"
+ nacl.signing.SigningKey.generate()
.verify_key.to_curve25519_public_key()
.encode()
.hex()
)
if (
len(sids[i]) != 66
or not sids[i].startswith('05')
or not all(c in '0123456789ABCDEFabcdef' for c in sids[i])
):
print(f"Invalid session id: expected 66 hex digit id as first argument")

print(f"SOGS pubkey: {server_pk.hex()}")

for s in sids:
s = bytes.fromhex(s)

if s[0] == 0x05:
k25 = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(s[1:] + server_pk, digest_size=64, encoder=RawEncoder)
)

pk15 = sodium.crypto_scalarmult_ed25519_noclamp(k15, xed25519.pubkey(s[1:]))
pk25 = sodium.crypto_scalarmult_ed25519_noclamp(k25, xed25519.pubkey(s[1:]))

print(
f"{s.hex()} blinds to:\n - 15{pk15.hex()} or …{pk15[31] ^ 0x80:02x}\n - 25{pk25.hex()} or …{pk25[31] ^ 0x80:02x}"
)
111 changes: 111 additions & 0 deletions contrib/blind25-testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3

import sys
import nacl.bindings as sodium
import nacl.hash
import nacl.signing
from nacl.encoding import RawEncoder
from pyonionreq import xed25519

server_pk = bytes.fromhex("0000000000000000000000000000000000000000000000000000000000000001")

to_sign = "hello!"

for i in range(1000):
sk = nacl.signing.SigningKey.generate()
pk = sk.verify_key
xpk = pk.to_curve25519_public_key()
sid = "05" + xpk.encode().hex()

k25 = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(
bytes.fromhex(sid) + server_pk, digest_size=64, encoder=RawEncoder, key=b"SOGS_blind_v2"
)
)

# Comment notation:
# P = server pubkey
# a/A = ed25519 keypair
# b/B = x25519 keypair, converted from a/A
# S = session id = 0x05 || B
# T = |A|, that is, A with the sign bit cleared
# t = private scalar s.t. tG = T (which is ± the private scalar associated with A)
# k = blinding factor = H_64(S || P, key="SOGS_blind_v2")

# This is simulating what the blinding client (i.e. with full keys) can compute:

# k * A
pk25a = sodium.crypto_scalarmult_ed25519_noclamp(k25, pk.encode())
# -k * A
neg_k25 = sodium.crypto_core_ed25519_scalar_negate(k25)
pk25b = sodium.crypto_scalarmult_ed25519_noclamp(neg_k25, pk.encode())

# print(f"k: {k25.hex()}")
# print(f"-k: {neg_k25.hex()}")
#
# print(f"a: {pk25a.hex()}")
# print(f"b: {pk25b.hex()}")

assert pk25a != pk25b
assert pk25a[0:31] == pk25b[0:31]
assert pk25a[31] ^ 0x80 == pk25b[31]

# The one we want to use is what we would end up with *if* our Ed25519 had been positive (but of
# course there's a 50% chance it's negative).
ed_pk_is_positive = pk.encode()[31] & 0x80 == 0

pk25 = pk25a if ed_pk_is_positive else pk25b

###########
# Make sure we can get to pk25 from the session id
# We know sid and server_pk, so we can compute k25
T_pk25 = sodium.crypto_scalarmult_ed25519_noclamp(k25, xed25519.pubkey(xpk.encode()))
assert T_pk25 == pk25

# To sign something that validates with pk25 we have a bit more work

# First get our blinded, private scalar; we'll call it j

# We want to pick j such that it is always associated with |A|, that is, our positive pubkey,
# even if our pubkey is negative, so that someone with our session id can get our signing pubkey
# deterministically.

t = (
sk.to_curve25519_private_key().encode()
) # The value we get here is actually our private scalar, despite the name
if pk.encode()[31] & 0x80:
# If our actual pubkey is negative then negate j so that it is as if we are working from the
# positive version of our pubkey
t = sodium.crypto_core_ed25519_scalar_negate(t)

kt = sodium.crypto_core_ed25519_scalar_mul(k25, t)

kT = sodium.crypto_scalarmult_ed25519_base_noclamp(kt)
assert kT == pk25

# Now we more or less follow EdDSA, but with our blinded scalar instead of real scalar, and with
# a different hash function. (See comments in libsession-util config/groups/keys.cpp for more
# details).
hseed = nacl.hash.blake2b(
sk.encode()[0:31], key=b"SOGS25Seed", encoder=nacl.encoding.RawEncoder
)
r = sodium.crypto_core_ed25519_scalar_reduce(
nacl.hash.blake2b(
hseed + pk25 + to_sign.encode(), 64, key=b"SOGS25Sig", encoder=nacl.encoding.RawEncoder
)
)
R = sodium.crypto_scalarmult_ed25519_base_noclamp(r)

# S = r + H(R || A || M) a (with A=kT, a=kt)
hram = nacl.hash.sha512(R + kT + to_sign.encode(), encoder=nacl.encoding.RawEncoder)
S = sodium.crypto_core_ed25519_scalar_reduce(hram)
S = sodium.crypto_core_ed25519_scalar_mul(S, kt)
S = sodium.crypto_core_ed25519_scalar_add(S, r)

sig = R + S

###########################################
# Test bog standard Ed25519 signature verification:

vk = nacl.signing.VerifyKey(pk25)
vk.verify(to_sign.encode(), sig)
2 changes: 0 additions & 2 deletions contrib/pg-import.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@


with pgsql.transaction():

curin = old.cursor()
curout = pgsql.cursor()

Expand Down Expand Up @@ -131,7 +130,6 @@
curout.execute("ALTER TABLE rooms DROP CONSTRAINT room_image_fk")

def copy(table):

cols = [r['name'] for r in curin.execute(f"PRAGMA table_info({table})")]
if not cols:
raise RuntimeError(f"Expected table {table} does not exist in sqlite db")
Expand Down
2 changes: 1 addition & 1 deletion sogs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.3.8.dev0"
__version__ = "0.4.0.dev0"
75 changes: 28 additions & 47 deletions sogs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,6 @@ def parse_and_set_perm_flags(flags, perm_setting):
sys.exit(2)

elif update_room:

rooms = []
all_rooms = False
global_rooms = False
Expand Down Expand Up @@ -428,15 +427,15 @@ def parse_and_set_perm_flags(flags, perm_setting):

if args.add_moderators:
for a in args.add_moderators:
if not re.fullmatch(r'[01]5[A-Fa-f0-9]{64}', a):
if not re.fullmatch(r'[012]5[A-Fa-f0-9]{64}', a):
print(f"Error: '{a}' is not a valid session id", file=sys.stderr)
sys.exit(1)

sysadmin = SystemUser()

if global_rooms:
for sid in args.add_moderators:
u = User(session_id=sid, try_blinding=True)
u = User(session_id=sid)
u.set_moderator(admin=args.admin, visible=args.visible, added_by=sysadmin)
print(
"Added {} as {} global {}".format(
Expand All @@ -447,7 +446,7 @@ def parse_and_set_perm_flags(flags, perm_setting):
)
else:
for sid in args.add_moderators:
u = User(session_id=sid, try_blinding=True)
u = User(session_id=sid)
for room in rooms:
room.set_moderator(
u, admin=args.admin, visible=not args.hidden, added_by=sysadmin
Expand All @@ -464,57 +463,39 @@ def parse_and_set_perm_flags(flags, perm_setting):

if args.delete_moderators:
for a in args.delete_moderators:
if not re.fullmatch(r'[01]5[A-Fa-f0-9]{64}', a):
if not re.fullmatch(r'[012]5[A-Fa-f0-9]{64}', a):
print(f"Error: '{a}' is not a valid session id", file=sys.stderr)
sys.exit(1)

sysadmin = SystemUser()

if global_rooms:
for sid in args.delete_moderators:
u = User(session_id=sid, try_blinding=True)
was_admin = u.global_admin
if not u.global_admin and not u.global_moderator:
print(f"{u.session_id} was not a global moderator")
else:
u.remove_moderator(removed_by=sysadmin)
print(
f"Removed {u.session_id} as global {'admin' if was_admin else 'moderator'}"
)

if u.is_blinded and sid.startswith('05'):
try:
u2 = User(session_id=sid, try_blinding=False, autovivify=False)
if u2.global_admin or u2.global_moderator:
was_admin = u2.global_admin
u2.remove_moderator(removed_by=sysadmin)
print(
f"Removed {u2.session_id} as global "
f"{'admin' if was_admin else 'moderator'}"
)
except NoSuchUser:
pass
try:
u = User(session_id=sid, autovivify=False)
if u.global_admin or u.global_moderator:
was_admin = u.global_admin
u.remove_moderator(removed_by=sysadmin)
print(
f"Removed {u.session_id} "
f"(identified by {sid}) "
f"as global {'admin' if was_admin else 'moderator'}"
)
except NoSuchUser:
pass
else:
for sid in args.delete_moderators:
u = User(session_id=sid, try_blinding=True)
u2 = None
if u.is_blinded and sid.startswith('05'):
try:
u2 = User(session_id=sid, try_blinding=False, autovivify=False)
except NoSuchUser:
pass

for room in rooms:
room.remove_moderator(u, removed_by=sysadmin)
print(
f"Removed {u.session_id} as moderator/admin of {room.name} ({room.token})"
)
if u2 is not None:
room.remove_moderator(u2, removed_by=sysadmin)
try:
u = User(session_id=sid, autovivify=False)
for room in rooms:
room.remove_moderator(u, removed_by=sysadmin)
print(
f"Removed {u2.session_id} as moderator/admin of {room.name} "
f"({room.token})"
f"Removed {u.session_id} "
f"(identified by {sid}) "
f"as moderator/admin of {room.name} ({room.token})"
)
except NoSuchUser:
pass

if args.add_perms or args.clear_perms or args.remove_perms:
if global_rooms:
Expand All @@ -524,9 +505,10 @@ def parse_and_set_perm_flags(flags, perm_setting):
)
sys.exit(1)

vivify = args.add_perms or args.remove_perms
users = []
if args.users:
users = [User(session_id=sid, try_blinding=True) for sid in args.users]
users = [User(session_id=sid, autovivify=vivify) for sid in args.users]

# users not specified means set room defaults
if not len(users):
Expand Down Expand Up @@ -577,8 +559,7 @@ def parse_and_set_perm_flags(flags, perm_setting):
if args.name is not None:
if global_rooms or all_rooms:
print(
"Error: --rooms cannot be '+' or '*' (i.e. global/all) with --name",
file=sys.stderr,
"Error: --rooms cannot be '+' or '*' (i.e. global/all) with --name", file=sys.stderr
)
sys.exit(1)

Expand Down
6 changes: 5 additions & 1 deletion sogs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
ALPHABET_SILENT = True
FILTER_MODS = False
REQUIRE_BLIND_KEYS = True
REQUIRE_BLIND_V2 = False
TEMPLATE_PATH = 'templates'
STATIC_PATH = 'static'
UPLOAD_PATH = 'uploads'
Expand Down Expand Up @@ -147,7 +148,10 @@ def reply_to_format(v):
'active_prune_threshold': ('ROOM_ACTIVE_PRUNE_THRESHOLD', None, days_to_seconds),
},
'direct_messages': {'expiry': ('DM_EXPIRY', None, days_to_seconds)},
'users': {'require_blind_keys': bool_opt('REQUIRE_BLIND_KEYS')},
'users': {
'require_blind_keys': bool_opt('REQUIRE_BLIND_KEYS'),
'require_blind_v2': bool_opt('REQUIRE_BLIND_V2'),
},
'messages': {
'history_prune_threshold': ('MESSAGE_HISTORY_PRUNE_THRESHOLD', None, days_to_seconds),
'profanity_filter': bool_opt('PROFANITY_FILTER'),
Expand Down
Loading