Skip to content

Commit 727b9ac

Browse files
authored
Merge pull request #404 from cfrg/caw/x25519-vectors
Add x25519 AKE test vectors
2 parents 7b23be5 + 7360d57 commit 727b9ac

12 files changed

+1544
-468
lines changed

draft-irtf-cfrg-opaque.md

+506-180
Large diffs are not rendered by default.

poc/ake_group.sage

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import sys
2+
import hashlib
3+
4+
########## Definitions from RFC 7748 ##################
5+
from sagelib.rfc7748 import *
6+
from sagelib.groups import *
7+
from sagelib.string_utils import *
8+
9+
try:
10+
from sagelib.opaque_common import curve25519_clamp
11+
except ImportError as e:
12+
sys.exit("Error loading preprocessed sage files. Try running `make setup && make clean pyfiles`. Full error: " + e)
13+
14+
15+
class GroupCurve25519(Group):
16+
def __init__(self):
17+
Group.__init__(self, "curve25519")
18+
19+
def generator(self):
20+
return IntegerToByteArray(9)
21+
22+
def serialize(self, element):
23+
# Curve25519 points are bytes
24+
return element
25+
26+
def deserialize(self, encoded):
27+
# Curve25519 points are bytes
28+
return encoded
29+
30+
def serialize_scalar(self, scalar):
31+
# Curve25519 scalars are represented as bytes
32+
return scalar
33+
34+
def element_byte_length(self):
35+
return 32
36+
37+
def scalar_byte_length(self):
38+
return 32
39+
40+
def random_scalar(self, rng):
41+
return curve25519_clamp(rng.random_bytes(32))
42+
43+
def scalar_mult(self, x, y):
44+
return X25519(x, y)
45+
46+
def __str__(self):
47+
return self.name
48+
49+
if __name__ == "__main__":
50+
# From RFC7748: https://www.rfc-editor.org/rfc/rfc7748#section-6.1
51+
a = bytes.fromhex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
52+
A = bytes.fromhex("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
53+
G = GroupCurve25519()
54+
A_exp = G.scalar_mult(a, G.generator())
55+
assert(A_exp == A)

poc/format_test_vectors.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,8 @@
3333
"server_public_key",
3434
"server_nonce",
3535
"client_nonce",
36-
"server_public_keyshare",
37-
"client_keyshare",
38-
"server_private_keyshare",
39-
"client_private_keyshare",
36+
"client_keyshare_seed",
37+
"server_keyshare_seed",
4038
"blind_registration",
4139
"blind_login",
4240
]
@@ -64,7 +62,7 @@
6462
"session_key",
6563
]
6664

67-
### Fake Vector Keys
65+
# Fake Vector Keys
6866

6967
fake_input_keys = [
7068
"client_identity",
@@ -80,10 +78,8 @@
8078
"server_public_key",
8179
"server_nonce",
8280
"client_nonce",
83-
"server_public_keyshare",
84-
"client_keyshare",
85-
"server_private_keyshare",
86-
"client_private_keyshare",
81+
"client_keyshare_seed",
82+
"server_keyshare_seed",
8783
"blind_registration",
8884
"blind_login",
8985
"masking_key",
@@ -94,6 +90,7 @@
9490
"KE2",
9591
]
9692

93+
9794
def to_hex(octet_string):
9895
if isinstance(octet_string, str):
9996
return "".join("{:02x}".format(ord(c)) for c in octet_string)
@@ -102,40 +99,47 @@ def to_hex(octet_string):
10299
assert isinstance(octet_string, bytearray)
103100
return ''.join(format(x, '02x') for x in octet_string)
104101

102+
105103
def wrap_print(arg, *args):
106104
line_length = 69
107105
string = arg + " " + " ".join(args)
108106
for hunk in (string[0+i:line_length+i] for i in range(0, len(string), line_length)):
109107
if hunk and len(hunk.strip()) > 0:
110108
print(hunk)
111109

110+
112111
def format_vector_name(vector):
113112
return "OPAQUE-" + vector["config"]["Name"]
114113

114+
115115
def print_vector_config(vector):
116116
for key in config_keys:
117117
for config_key in vector["config"]:
118118
if key == config_key:
119119
wrap_print(key + ":", vector["config"][key])
120120

121+
121122
def print_vector_inputs(arr, vector):
122123
for key in arr:
123124
for input_key in vector["inputs"]:
124125
if key == input_key:
125126
wrap_print(key + ":", vector["inputs"][key])
126127

128+
127129
def print_vector_intermediates(arr, vector):
128130
for key in arr:
129131
for int_key in vector["intermediates"]:
130132
if key == int_key:
131133
wrap_print(key + ":", vector["intermediates"][key])
132134

135+
133136
def print_vector_outputs(arr, vector):
134137
for key in arr:
135138
for output_key in vector["outputs"]:
136139
if key == output_key:
137140
wrap_print(key + ":", vector["outputs"][key])
138141

142+
139143
def format_vector(vector, i):
140144
print("\n#### Configuration\n")
141145
print("~~~")
@@ -155,6 +159,7 @@ def format_vector(vector, i):
155159
print("~~~")
156160
print("")
157161

162+
158163
def format_fake_vector(vector, i):
159164
print("\n#### Configuration\n")
160165
print("~~~")
@@ -170,6 +175,7 @@ def format_fake_vector(vector, i):
170175
print("~~~")
171176
print("")
172177

178+
173179
with open(sys.argv[1], "r") as fh:
174180
vectors = json.loads(fh.read())
175181
real_vectors = []
@@ -181,10 +187,12 @@ def format_fake_vector(vector, i):
181187
real_vectors.append(vector)
182188
print("## Real Test Vectors {#real-vectors}\n")
183189
for i, vector in enumerate(real_vectors):
184-
print("### " + format_vector_name(vector) + " Real Test Vector " + str(i+1))
190+
print("### " + format_vector_name(vector) +
191+
" Real Test Vector " + str(i+1))
185192
format_vector(vector, i)
186193

187194
print("## Fake Test Vectors {#fake-vectors}\n")
188195
for i, vector in enumerate(fake_vectors):
189-
print("### " + format_vector_name(vector) + " Fake Test Vector " + str(i+1))
196+
print("### " + format_vector_name(vector) +
197+
" Fake Test Vector " + str(i+1))
190198
format_fake_vector(vector, i)

poc/opaque_ake.sage

+35-37
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ from collections import namedtuple
88

99
try:
1010
from sagelib.opaque_common import derive_secret, hkdf_expand_label, hkdf_extract, I2OSP, OS2IP, OS2IP_le, encode_vector, encode_vector_len, to_hex, OPAQUE_NONCE_LENGTH
11-
from sagelib.opaque_core import OPAQUECore
11+
from sagelib.opaque_core import OPAQUECore, OPAQUE_SEED_LENGTH
1212
from sagelib.opaque_messages import deserialize_credential_request, deserialize_credential_response
1313
except ImportError as e:
1414
sys.exit("Error loading preprocessed sage files. Try running `make setup && make clean pyfiles`. Full error: " + e)
@@ -76,9 +76,9 @@ class OPAQUE3DH(KeyExchange):
7676
}
7777

7878
def derive_3dh_keys(self, dh_components, info):
79-
dh1 = dh_components.sk1 * dh_components.pk1
80-
dh2 = dh_components.sk2 * dh_components.pk2
81-
dh3 = dh_components.sk3 * dh_components.pk3
79+
dh1 = self.config.group.scalar_mult(dh_components.sk1, self.config.group.deserialize(dh_components.pk1))
80+
dh2 = self.config.group.scalar_mult(dh_components.sk2, self.config.group.deserialize(dh_components.pk2))
81+
dh3 = self.config.group.scalar_mult(dh_components.sk3, self.config.group.deserialize(dh_components.pk3))
8282

8383
dh1_encoded = self.config.group.serialize(dh1)
8484
dh2_encoded = self.config.group.serialize(dh2)
@@ -101,10 +101,9 @@ class OPAQUE3DH(KeyExchange):
101101

102102
def auth_client_start(self):
103103
self.client_nonce = self.rng.random_bytes(OPAQUE_NONCE_LENGTH)
104-
self.client_private_keyshare = ZZ(self.config.group.random_scalar(self.rng))
105-
self.client_public_keyshare_bytes = self.config.group.serialize(self.client_private_keyshare * self.config.group.generator())
106-
107-
return TripleDHMessageInit(self.client_nonce, self.client_public_keyshare_bytes)
104+
self.client_keyshare_seed = self.rng.random_bytes(OPAQUE_SEED_LENGTH)
105+
self.client_private_keyshare, self.client_public_keyshare = self.core.derive_diffie_hellman_key_pair(self.client_keyshare_seed)
106+
return TripleDHMessageInit(self.client_nonce, self.client_public_keyshare)
108107

109108
def generate_ke1(self, password):
110109
cred_request, cred_metadata = self.core.create_credential_request(password)
@@ -116,14 +115,14 @@ class OPAQUE3DH(KeyExchange):
116115

117116
return self.serialized_request + ke1.serialize()
118117

119-
def transcript_hasher(self, serialized_request, serialized_response, cleartext_credentials, client_nonce, client_public_keyshare_bytes, server_nonce, server_public_keyshare_bytes):
118+
def transcript_hasher(self, serialized_request, serialized_response, cleartext_credentials, client_nonce, client_public_keyshare, server_nonce, server_public_keyshare_bytes):
120119
hasher = self.config.hash()
121120
hasher.update(_as_bytes("RFCXXXX")) # RFCXXXX
122121
hasher.update(encode_vector(self.config.context)) # context
123122
hasher.update(encode_vector_len(cleartext_credentials.client_identity, 2)) # client_identity
124123
hasher.update(serialized_request) # ke1: cred request
125124
hasher.update(client_nonce) # ke1: client nonce
126-
hasher.update(client_public_keyshare_bytes) # ke1: client keyshare
125+
hasher.update(client_public_keyshare) # ke1: client keyshare
127126
hasher.update(encode_vector_len(cleartext_credentials.server_identity, 2)) # server identity
128127
hasher.update(serialized_response) # ke2: cred response
129128
hasher.update(server_nonce) # ke2: server nonce
@@ -133,21 +132,19 @@ class OPAQUE3DH(KeyExchange):
133132

134133
return hasher.digest()
135134

136-
def auth_server_respond(self, cred_request, cred_response, ke1, cleartext_credentials, server_private_key, client_public_key):
135+
def auth_server_respond(self, cred_request, cred_response, ke1, cleartext_credentials, server_private_key, client_public_keyshare):
137136
self.server_nonce = self.rng.random_bytes(OPAQUE_NONCE_LENGTH)
138-
self.server_private_keyshare = ZZ(self.config.group.random_scalar(self.rng))
139-
self.server_public_keyshare = self.server_private_keyshare * self.config.group.generator()
140-
server_public_keyshare_bytes = self.config.group.serialize(self.server_public_keyshare)
141-
client_public_keyshare = self.config.group.deserialize(ke1.client_public_keyshare)
137+
self.server_keyshare_seed = self.rng.random_bytes(OPAQUE_SEED_LENGTH)
138+
self.server_private_keyshare, self.server_public_keyshare_bytes = self.core.derive_diffie_hellman_key_pair(self.server_keyshare_seed)
142139

143-
transcript_hash = self.transcript_hasher(cred_request.serialize(), cred_response.serialize(), cleartext_credentials, ke1.client_nonce, ke1.client_public_keyshare, self.server_nonce, server_public_keyshare_bytes)
140+
transcript_hash = self.transcript_hasher(cred_request.serialize(), cred_response.serialize(), cleartext_credentials, ke1.client_nonce, ke1.client_public_keyshare, self.server_nonce, self.server_public_keyshare_bytes)
144141

145142
# K3dh = epkU^eskS || epkU^skS || pkU^eskS
146-
dh_components = TripleDHComponents(client_public_keyshare, self.server_private_keyshare, client_public_keyshare, server_private_key, client_public_key, self.server_private_keyshare)
143+
dh_components = TripleDHComponents(ke1.client_public_keyshare, self.server_private_keyshare, ke1.client_public_keyshare, server_private_key, client_public_keyshare, self.server_private_keyshare)
147144

148145
server_mac_key, client_mac_key, session_key, handshake_secret = self.derive_3dh_keys(dh_components, self.hasher.digest())
149146
mac = hmac.digest(server_mac_key, transcript_hash, self.config.hash)
150-
ake2 = TripleDHMessageRespond(self.server_nonce, server_public_keyshare_bytes, mac)
147+
ake2 = TripleDHMessageRespond(self.server_nonce, self.server_public_keyshare_bytes, mac)
151148

152149
self.server_mac_key = server_mac_key
153150
self.ake2 = ake2
@@ -158,27 +155,24 @@ class OPAQUE3DH(KeyExchange):
158155

159156
return ake2
160157

161-
def generate_ke2(self, msg, oprf_seed, credential_identifier, envU, masking_key, server_identity, server_private_key, server_public_key, client_identity, client_public_key):
158+
def generate_ke2(self, msg, oprf_seed, credential_identifier, envU, masking_key, server_identity, server_private_key, server_public_keyshare, client_identity, client_public_keyshare):
162159
cred_request, offset = deserialize_credential_request(self.config, msg)
163160
ke1 = deserialize_tripleDH_init(self.config, msg[offset:])
164161

165-
server_public_key_bytes = self.config.group.serialize(server_public_key)
166-
cred_response = self.core.create_credential_response(cred_request, server_public_key_bytes, oprf_seed, envU, credential_identifier, masking_key)
162+
cred_response = self.core.create_credential_response(cred_request, server_public_keyshare, oprf_seed, envU, credential_identifier, masking_key)
167163
serialized_response = cred_response.serialize()
168164
self.masking_nonce = cred_response.masking_nonce
169165

170-
cleartext_credentials = self.core.create_cleartext_credentials(server_public_key_bytes, self.config.group.serialize(client_public_key), server_identity, client_identity)
171-
ake2 = self.auth_server_respond(cred_request, cred_response, ke1, cleartext_credentials, server_private_key, client_public_key)
166+
cleartext_credentials = self.core.create_cleartext_credentials(server_public_keyshare, client_public_keyshare, server_identity, client_identity)
167+
ake2 = self.auth_server_respond(cred_request, cred_response, ke1, cleartext_credentials, server_private_key, client_public_keyshare)
172168

173169
return serialized_response + ake2.serialize()
174170

175-
def auth_client_finalize(self, cred_response, ake2, cleartext_credentials, client_private_key, client_public_key):
176-
transcript_hash = self.transcript_hasher(self.serialized_request, cred_response.serialize(), cleartext_credentials, self.client_nonce, self.client_public_keyshare_bytes, ake2.server_nonce, ake2.server_public_keyshare_bytes)
177-
server_public_key = self.config.group.deserialize(cleartext_credentials.server_public_key_bytes)
178-
server_public_keyshare = self.config.group.deserialize(ake2.server_public_keyshare_bytes)
171+
def auth_client_finalize(self, cred_response, ake2, cleartext_credentials, client_private_key):
172+
transcript_hash = self.transcript_hasher(self.serialized_request, cred_response.serialize(), cleartext_credentials, self.client_nonce, self.client_public_keyshare, ake2.server_nonce, ake2.server_public_keyshare_bytes)
179173

180174
# K3dh = epkS^eskU || pkS^eskU || epkS^skU
181-
dh_components = TripleDHComponents(server_public_keyshare, self.client_private_keyshare, server_public_key, self.client_private_keyshare, server_public_keyshare, client_private_key)
175+
dh_components = TripleDHComponents(ake2.server_public_keyshare_bytes, self.client_private_keyshare, cleartext_credentials.server_public_key_bytes, self.client_private_keyshare, ake2.server_public_keyshare_bytes, client_private_key)
182176

183177
server_mac_key, client_mac_key, session_key, handshake_secret = self.derive_3dh_keys(dh_components, self.hasher.digest())
184178
server_mac = hmac.digest(server_mac_key, transcript_hash, self.config.hash)
@@ -197,16 +191,20 @@ class OPAQUE3DH(KeyExchange):
197191

198192
return TripleDHMessageFinish(client_mac)
199193

200-
def generate_ke3(self, msg, client_identity, client_public_key, server_identity):
194+
def generate_ke3(self, msg, client_identity, server_identity):
201195
cred_response, offset = deserialize_credential_response(self.config, msg)
202196
ake2 = deserialize_tripleDH_respond(self.config, msg[offset:])
203197
client_private_key_bytes, cleartext_credentials, export_key = self.core.recover_credentials(self.password, self.cred_metadata, cred_response, client_identity, server_identity)
204-
client_private_key = OS2IP(client_private_key_bytes)
205-
if "ristretto" in self.config.group.name or "decaf" in self.config.group.name:
198+
199+
if "curve25519" in self.config.group.name:
200+
client_private_key = client_private_key_bytes
201+
elif "ristretto" in self.config.group.name or "decaf" in self.config.group.name:
206202
client_private_key = OS2IP_le(client_private_key_bytes)
207-
self.export_key = export_key
203+
else:
204+
client_private_key = OS2IP(client_private_key_bytes)
208205

209-
ke3 = self.auth_client_finalize(cred_response, ake2, cleartext_credentials, client_private_key, client_public_key)
206+
self.export_key = export_key
207+
ke3 = self.auth_client_finalize(cred_response, ake2, cleartext_credentials, client_private_key)
210208

211209
return ke3.serialize()
212210

@@ -228,11 +226,11 @@ class OPAQUE3DH(KeyExchange):
228226
# } KE1M;
229227
def deserialize_tripleDH_init(config, data):
230228
client_nonce = data[0:OPAQUE_NONCE_LENGTH]
231-
client_public_keyshare_bytes = data[OPAQUE_NONCE_LENGTH:]
229+
client_public_keyshare = data[OPAQUE_NONCE_LENGTH:]
232230
length = config.oprf_suite.group.element_byte_length()
233-
if len(client_public_keyshare_bytes) != length:
234-
raise Exception("Invalid client_public_keyshare length: %d %d" % (len(client_public_keyshare_bytes), length))
235-
return TripleDHMessageInit(client_nonce, client_public_keyshare_bytes)
231+
if len(client_public_keyshare) != length:
232+
raise Exception("Invalid client_public_keyshare length: %d %d" % (len(client_public_keyshare), length))
233+
return TripleDHMessageInit(client_nonce, client_public_keyshare)
236234

237235
class TripleDHMessageInit(object):
238236
def __init__(self, client_nonce, client_public_keyshare):

poc/opaque_common.sage

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ def xor(a, b):
2828
c[i] = c[i] ^^ v # bitwise XOR
2929
return bytes(c)
3030

31+
# Performs the curve25519 clamping operation
32+
def curve25519_clamp(scalar):
33+
arr = bytearray(scalar)
34+
arr[0] &= 248
35+
arr[31] &= 127
36+
arr[31] |= 64
37+
return bytes(arr)
38+
3139
def hkdf_extract(config, salt, ikm):
3240
return hmac.digest(salt, ikm, config.hash)
3341

0 commit comments

Comments
 (0)