1
+ #!/usr/bin/env python3
2
+
1
3
# Example script for demonstrating X-SOGS-* authentication calculation.
2
4
3
5
import nacl .bindings as sodium
4
6
from nacl .signing import SigningKey
5
7
from hashlib import blake2b , sha512
6
8
from base64 import b64encode
7
9
8
- # import time
9
- # import nacl.utils
10
+ import time
11
+ import nacl .utils
12
+
13
+ import argparse
14
+ import sys
15
+ import requests
10
16
11
17
12
18
def sha512_multipart (* message_parts ):
@@ -22,6 +28,23 @@ def sha512_multipart(*message_parts):
22
28
return hasher .digest ()
23
29
24
30
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
+
25
48
def blinded_ed25519_signature (message_parts , s : SigningKey , ka : bytes , kA : bytes ):
26
49
"""
27
50
Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with
@@ -53,18 +76,7 @@ def get_signing_headers(
53
76
assert len (nonce ) == 16
54
77
55
78
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 )
68
80
69
81
# Blinded session id:
70
82
pubkey = '15' + kA .hex ()
@@ -98,26 +110,149 @@ def get_signing_headers(
98
110
}
99
111
100
112
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
+
101
186
# Session "master" ed25519 key:
102
- s = SigningKey (bytes .fromhex ('c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9' ))
187
+ s = SigningKey (bytes .fromhex (args . seed ))
103
188
# Server pubkey:
104
- B = bytes .fromhex ('c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d' )
189
+ B = bytes .fromhex (args . server_pubkey )
105
190
# 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 ("\n Blinded 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"\n Submitting 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 ("\n Unblinded 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 ("\n Blinded 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 )
121
256
122
257
123
258
# Prints:
0 commit comments