Skip to content

Commit c479edf

Browse files
committed
Parameterize & support submission from auth-example.py
Lets you use it to generate headers for arbitrary URLs, and to also use it to submit an authenticated request to a SOGS.
1 parent 66f31ed commit c479edf

File tree

1 file changed

+166
-31
lines changed

1 file changed

+166
-31
lines changed

contrib/auth-example.py

100644100755
Lines changed: 166 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
#!/usr/bin/env python3
2+
13
# Example script for demonstrating X-SOGS-* authentication calculation.
24

35
import nacl.bindings as sodium
46
from nacl.signing import SigningKey
57
from hashlib import blake2b, sha512
68
from base64 import b64encode
79

8-
# import time
9-
# import nacl.utils
10+
import time
11+
import nacl.utils
12+
13+
import argparse
14+
import sys
15+
import requests
1016

1117

1218
def sha512_multipart(*message_parts):
@@ -22,6 +28,23 @@ def sha512_multipart(*message_parts):
2228
return hasher.digest()
2329

2430

31+
def blinded_ed25519_keys(server_pk: bytes, s: SigningKey):
32+
# 64-byte blake2b hash then reduce to get the blinding factor:
33+
k = sodium.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
34+
35+
# Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
36+
# convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
37+
# same secret scalar secret. (And so this is just the most convenient way to get 'a' out of
38+
# a sodium Ed25519 secret key).
39+
a = s.to_curve25519_private_key().encode()
40+
41+
# Our blinded keypair:
42+
ka = sodium.crypto_core_ed25519_scalar_mul(k, a)
43+
kA = sodium.crypto_scalarmult_ed25519_base_noclamp(ka)
44+
45+
return ka, kA
46+
47+
2548
def blinded_ed25519_signature(message_parts, s: SigningKey, ka: bytes, kA: bytes):
2649
"""
2750
Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with
@@ -53,18 +76,7 @@ def get_signing_headers(
5376
assert len(nonce) == 16
5477

5578
if blinded:
56-
# 64-byte blake2b hash then reduce to get the blinding factor:
57-
k = sodium.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
58-
59-
# Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
60-
# convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
61-
# same secret scalar secret. (And so this is just the most convenient way to get 'a' out of
62-
# a sodium Ed25519 secret key).
63-
a = s.to_curve25519_private_key().encode()
64-
65-
# Our blinded keypair:
66-
ka = sodium.crypto_core_ed25519_scalar_mul(k, a)
67-
kA = sodium.crypto_scalarmult_ed25519_base_noclamp(ka)
79+
ka, kA = blinded_ed25519_keys(server_pk, s)
6880

6981
# Blinded session id:
7082
pubkey = '15' + kA.hex()
@@ -98,26 +110,149 @@ def get_signing_headers(
98110
}
99111

100112

113+
#
114+
# End of blinding cryptography; everything below this is command-line parsing, display, etc.
115+
#
116+
117+
118+
def hexstr(size: int):
119+
def validator(x: str):
120+
import string
121+
122+
if len(x) != size or not all(c in string.hexdigits for c in x):
123+
raise RuntimeError(f"Invalid argument: {x}; expected {size}-char hex string")
124+
125+
126+
parser = argparse.ArgumentParser(description="auth test")
127+
parser.add_argument(
128+
'--seed',
129+
'-s',
130+
type=hexstr(64),
131+
default='c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9',
132+
help='Ed25519 seed hex',
133+
)
134+
parser.add_argument(
135+
'--blinded', '-b', action='store_true', help='Specify to generated blinded auth headers'
136+
)
137+
parser.add_argument(
138+
'--unblinded', '-u', action='store_true', help='Specify to generate unblinded auth headers'
139+
)
140+
parser.add_argument(
141+
'--server-pubkey',
142+
'-k',
143+
type=hexstr(64),
144+
default='c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d',
145+
help='Server X25519 pubkey (hex)',
146+
)
147+
parser.add_argument(
148+
'--nonce',
149+
'-n',
150+
type=hexstr(32),
151+
default='09d0799f2295990182c3ab3406fbfc5b',
152+
help='Request nonce (hex)',
153+
)
154+
parser.add_argument(
155+
'--random-nonce', '-N', action='store_true', help='Use random nonce instead of --nonce value'
156+
)
157+
parser.add_argument(
158+
'--timestamp',
159+
'-t',
160+
type=int,
161+
default=1642472103,
162+
help='Request timestamp; specify 0 for current time',
163+
)
164+
parser.add_argument('--method', '-m', type=str, default='GET', help='Request method, e.g. GET POST')
165+
parser.add_argument(
166+
'--path',
167+
'-p',
168+
type=str,
169+
default='/room/the-best-room/messages/recent?limit=25',
170+
help='Request path',
171+
)
172+
parser.add_argument('--body', '-B', type=str, help='Request body (for POST, etc.)')
173+
parser.add_argument(
174+
'--submit',
175+
'-S',
176+
type=str,
177+
help='Submit the request to this URL; takes the base URL (i.e. without the path)',
178+
)
179+
180+
args = parser.parse_args()
181+
182+
if not (args.blinded or args.unblinded):
183+
args.blinded = True
184+
args.unblinded = True
185+
101186
# Session "master" ed25519 key:
102-
s = SigningKey(bytes.fromhex('c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9'))
187+
s = SigningKey(bytes.fromhex(args.seed))
103188
# Server pubkey:
104-
B = bytes.fromhex('c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d')
189+
B = bytes.fromhex(args.server_pubkey)
105190
# Random 16-byte nonce
106-
# nonce = nacl.utils.random(16)
107-
nonce = bytes.fromhex('09d0799f2295990182c3ab3406fbfc5b') # fixed for reproducible example
108-
# ts = int(time.time())
109-
ts = 1642472103 # for reproducible example
110-
method, path = 'GET', '/room/the-best-room/messages/recent?limit=25'
111-
112-
print("Unblinded headers:")
113-
sig_headers = get_signing_headers(s, B, nonce, method, path, ts, body=None, blinded=False)
114-
for h, v in sig_headers.items():
115-
print(f"{h}: {v}")
116-
117-
print("\nBlinded headers:")
118-
sig_headers = get_signing_headers(s, B, nonce, method, path, ts, body=None, blinded=True)
119-
for h, v in sig_headers.items():
120-
print(f"{h}: {v}")
191+
if args.random_nonce:
192+
nonce = nacl.utils.random(16)
193+
else:
194+
nonce = bytes.fromhex(args.nonce)
195+
ts = args.timestamp
196+
if ts == 0:
197+
ts = int(time.time())
198+
method = args.method.upper()
199+
if method not in ('GET', 'POST', 'PUT', 'DELETE'):
200+
print(f"Error: invalid method {method}", file=sys.stderr)
201+
sys.exit(1)
202+
path = args.path
203+
if not path.startswith('/'):
204+
print(f"Error: invalid path {path}: should start with a /", file=sys.stderr)
205+
sys.exit(1)
206+
body = args.body
207+
208+
if body is not None and method not in ('POST', 'PUT'):
209+
print(f"Error: {method} request should not have a body", file=sys.stderr)
210+
sys.exit(1)
211+
212+
213+
def submit_req(headers):
214+
url = args.submit.rstrip('/') + path
215+
print(f"\nSubmitting request to {url}...\n", file=sys.stderr)
216+
r = requests.request(method, url, headers=sig_headers, data=body)
217+
print(f"Request returned {r.status_code} {r.reason} with headers:", file=sys.stderr)
218+
for k, v in r.headers.items():
219+
print(f" {k}: {v}", file=sys.stderr)
220+
print("Body:", file=sys.stderr)
221+
print(r.text)
222+
223+
224+
print(
225+
f"""
226+
Signing request using:
227+
Master Ed25519 pubkey: {s.verify_key.encode().hex()}
228+
Session ID: 05{s.to_curve25519_private_key().public_key.encode().hex()}
229+
Server X25519 pubkey: {B.hex()}
230+
Blinded Session ID (for this server): 15{blinded_ed25519_keys(B, s)[1].hex()}
231+
Request: {method} {path}
232+
""",
233+
file=sys.stderr,
234+
)
235+
236+
237+
if args.unblinded:
238+
print("\nUnblinded headers:", file=sys.stderr)
239+
sig_headers = get_signing_headers(s, B, nonce, method, path, ts, body=body, blinded=False)
240+
for h, v in sig_headers.items():
241+
print(f"{h}: {v}", file=sys.stderr)
242+
if args.submit:
243+
submit_req(sig_headers)
244+
245+
246+
if args.blinded:
247+
if args.unblinded and args.random_nonce:
248+
nonce = nacl.utils.random(16)
249+
250+
print("\nBlinded headers:", file=sys.stderr)
251+
sig_headers = get_signing_headers(s, B, nonce, method, path, ts, body=body, blinded=True)
252+
for h, v in sig_headers.items():
253+
print(f"{h}: {v}", file=sys.stderr)
254+
if args.submit:
255+
submit_req(sig_headers)
121256

122257

123258
# Prints:

0 commit comments

Comments
 (0)