beaconcrypt 0.3.4__tar.gz → 0.3.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/Cargo.lock +1 -1
  2. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/Cargo.toml +1 -1
  3. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/PKG-INFO +1 -1
  4. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/README.md +15 -0
  5. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/beaconcrypt.go +24 -1
  6. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/beaconcrypt_test.go +147 -9
  7. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/bindings.h +8 -0
  8. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/cnsa2.rs +1 -0
  9. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/lib.rs +45 -0
  10. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/pqxdh.rs +1 -0
  11. beaconcrypt-0.3.5/tests/protocol.rs +119 -0
  12. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/tests/test_encryption.py +78 -6
  13. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/tests/test_registration.py +15 -1
  14. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/.cargo/config.toml +0 -0
  15. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/.github/workflows/go.yml +0 -0
  16. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/.github/workflows/python.yml +0 -0
  17. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/.github/workflows/release.yml +0 -0
  18. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/.github/workflows/rust.yml +0 -0
  19. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/.gitignore +0 -0
  20. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/LICENSE +0 -0
  21. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/build.rs +0 -0
  22. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/clippy.toml +0 -0
  23. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/doc/protocol.md +0 -0
  24. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/doc/rationale.md +0 -0
  25. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/doc/threat_model.md +0 -0
  26. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/go.mod +0 -0
  27. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/pyproject.toml +0 -0
  28. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/rustfmt.toml +0 -0
  29. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/beacon.rs +0 -0
  30. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/error.rs +0 -0
  31. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/schema/cryptoframe.capnp +0 -0
  32. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/schema/phase1.capnp +0 -0
  33. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/schema/phase2.capnp +0 -0
  34. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/schema/protogram.capnp +0 -0
  35. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/server.rs +0 -0
  36. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/src/shared.rs +0 -0
  37. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/tests/beacon.rs +0 -0
  38. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/tests/server.rs +0 -0
  39. {beaconcrypt-0.3.4 → beaconcrypt-0.3.5}/uv.lock +0 -0
@@ -84,7 +84,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
84
84
 
85
85
  [[package]]
86
86
  name = "beaconcrypt"
87
- version = "0.3.4"
87
+ version = "0.3.5"
88
88
  dependencies = [
89
89
  "capnp",
90
90
  "capnpc",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "beaconcrypt"
3
- version = "0.3.4"
3
+ version = "0.3.5"
4
4
  edition = "2024"
5
5
  license-file = "LICENSE"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: beaconcrypt
3
- Version: 0.3.4
3
+ Version: 0.3.5
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
@@ -46,14 +46,29 @@ Test the C interface
46
46
  # Reference implementation
47
47
  I don't use rust a lot, so the code is probably fairly naive. It provides both a beacon and server implementation with C bindings through `cbindgen`. Ideally more bindings would be built on top of that so it can be used in the mythic server-side.
48
48
 
49
+ The reference implementation expects that all beacons are compiled with the server's public key, and that beaconcrypt is initialized with it.
50
+
51
+ The server is currently not very usable as it doesn't support saving the state of any individual beacon. This means that if your server goes down, you will not be able to communicate with any previously-registered beacons anymore. The server doesn't support being initialized with an Ed25519 seed (32 random bytes). Users wishing to use the server in practical cases should use this interface to ensure their server keeps its identity across reboots.
52
+
49
53
  ## Building
50
54
  You will need [Capn'Proto](https://capnproto.org/install.html) (just the binaries) and a recent version of rust for every build.
51
55
 
52
56
  For windows, I prefer building with stable-gnu for normal usage, and nightly-gnu for release builds. You can find the exact arguments I use to the the static library as small as possible in [release.yml](/.github\workflows\release.yml). The MSVC toolchain is expected to work just as well, I just like mingw.
53
57
 
58
+ Build and run all tests:
59
+ ```bash
60
+ cargo test
61
+ cargo build --target x86_64-pc-windows-gnu
62
+ go test ./...
63
+ uv run maturin build --features pybinds --uv
64
+ uv run --no-sync pytest tests
65
+ ```
66
+
54
67
  ## Profiles
55
68
  The reference implementation has two profiles: `PQXDH` and `CNSA2`. Profiles are controlled by cargo features. The CNSA2 profile only exists as a test for now. It uses a simple ML-KEM encapsulation for key exchange and the underlying libraries are not FIPS-approved. It is experimental and is likely broken. PQXDH is the intended target and the default.
56
69
 
70
+ **CNSA2 is currently completely broken**
71
+
57
72
  ## Usage
58
73
  The reference implementation is a library that can currently be used either from rust or through C FFI. The C interface is currently not tested.
59
74
 
@@ -33,7 +33,9 @@ beaconcrypt_go_buffer beaconcrypt_go_process_initial_message(void *handle, const
33
33
  beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_beacon(void *handle, uint64_t key_id, const uint8_t *ptr, uintptr_t len);
34
34
  beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_beacon_signed(void *handle, uint64_t key_id, const uint8_t *ptr, uintptr_t len);
35
35
  beaconcrypt_go_buffer beaconcrypt_go_decrypt_beacon_message(void *handle, uint64_t key_id, const uint8_t *ptr, uintptr_t len);
36
+ beaconcrypt_go_buffer beaconcrypt_go_decrypt_beacon_message_signed(void *handle, const uint8_t *ptr, uintptr_t len);
36
37
  beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_server(void *handle, const uint8_t *ptr, uintptr_t len);
38
+ beaconcrypt_go_buffer beaconcrypt_go_encrypt_to_server_signed(void *handle, const uint8_t *ptr, uintptr_t len);
37
39
  beaconcrypt_go_buffer beaconcrypt_go_decrypt_server_message(void *handle, const uint8_t *ptr, uintptr_t len);
38
40
  beaconcrypt_go_buffer beaconcrypt_go_decrypt_server_message_signed(void *handle, const uint8_t *ptr, uintptr_t len);
39
41
  */
@@ -203,6 +205,15 @@ func (s *Server) DecryptBeaconMessage(keyID uint64, ciphertext []byte) ([]byte,
203
205
  })
204
206
  }
205
207
 
208
+ func (s *Server) DecryptBeaconMessageSigned(ciphertext []byte) ([]byte, error) {
209
+ if s == nil || s.handle == nil {
210
+ return nil, ErrClosed
211
+ }
212
+ return callUnary(ciphertext, func(ptr *C.uint8_t, len C.uintptr_t) C.beaconcrypt_go_buffer {
213
+ return C.beaconcrypt_go_decrypt_beacon_message_signed(s.handle, ptr, len)
214
+ })
215
+ }
216
+
206
217
  func (b *Beacon) EncryptToServer(plaintext []byte) ([]byte, error) {
207
218
  if b == nil || b.handle == nil {
208
219
  return nil, ErrClosed
@@ -212,6 +223,15 @@ func (b *Beacon) EncryptToServer(plaintext []byte) ([]byte, error) {
212
223
  })
213
224
  }
214
225
 
226
+ func (b *Beacon) EncryptToServerSigned(plaintext []byte) ([]byte, error) {
227
+ if b == nil || b.handle == nil {
228
+ return nil, ErrClosed
229
+ }
230
+ return callUnary(plaintext, func(ptr *C.uint8_t, len C.uintptr_t) C.beaconcrypt_go_buffer {
231
+ return C.beaconcrypt_go_encrypt_to_server_signed(b.handle, ptr, len)
232
+ })
233
+ }
234
+
215
235
  func (b *Beacon) DecryptServerMessage(ciphertext []byte) ([]byte, error) {
216
236
  if b == nil || b.handle == nil {
217
237
  return nil, ErrClosed
@@ -240,10 +260,13 @@ func callUnary(data []byte, call func(*C.uint8_t, C.uintptr_t) C.beaconcrypt_go_
240
260
  }
241
261
 
242
262
  func copyBuffer(buffer C.beaconcrypt_go_buffer) ([]byte, error) {
243
- if buffer.ptr == nil || buffer.len == 0 {
263
+ if buffer.ptr == nil {
244
264
  return nil, ErrCrypto
245
265
  }
246
266
  defer C.beaconcrypt_go_free_buffer(buffer)
267
+ if buffer.len == 0 {
268
+ return []byte{}, nil
269
+ }
247
270
  return C.GoBytes(unsafe.Pointer(buffer.ptr), C.int(buffer.len)), nil
248
271
  }
249
272
 
@@ -11,27 +11,34 @@ func registerBeacon(t *testing.T, server *Server, beacon *Beacon) []byte {
11
11
  t.Helper()
12
12
 
13
13
  message := bytes.Repeat([]byte{0xff}, 32)
14
- phase1, err := beacon.GenerateRegistration()
14
+ regOut := registerBeaconWithInitial(t, server, beacon, message)
15
+
16
+ phase2, err := beacon.ProcessInitialMessage(regOut.Serialized)
15
17
  if err != nil {
16
18
  t.Fatal(err)
17
19
  }
20
+ if !bytes.Equal(phase2, message) {
21
+ t.Fatalf("initial message mismatch: got %x want %x", phase2, message)
22
+ }
23
+ return phase2
24
+ }
18
25
 
19
- regOut, err := server.RegisterBeacon(phase1, message)
26
+ func registerBeaconWithInitial(t *testing.T, server *Server, beacon *Beacon, message []byte) *RegistrationResponse {
27
+ t.Helper()
28
+
29
+ phase1, err := beacon.GenerateRegistration()
20
30
  if err != nil {
21
31
  t.Fatal(err)
22
32
  }
23
- if regOut.KeyID == 0 {
24
- t.Fatal("expected non-zero beacon key id")
25
- }
26
33
 
27
- phase2, err := beacon.ProcessInitialMessage(regOut.Serialized)
34
+ regOut, err := server.RegisterBeacon(phase1, message)
28
35
  if err != nil {
29
36
  t.Fatal(err)
30
37
  }
31
- if !bytes.Equal(phase2, message) {
32
- t.Fatalf("initial message mismatch: got %x want %x", phase2, message)
38
+ if regOut.KeyID == 0 {
39
+ t.Fatal("expected non-zero beacon key id")
33
40
  }
34
- return phase2
41
+ return regOut
35
42
  }
36
43
 
37
44
  func newServer(t *testing.T) *Server {
@@ -69,6 +76,24 @@ func TestRegister(t *testing.T) {
69
76
  }
70
77
  }
71
78
 
79
+ func TestRegisterWithoutInitialMessage(t *testing.T) {
80
+ server := newServer(t)
81
+ serverPK, err := server.IdentityPK()
82
+ if err != nil {
83
+ t.Fatal(err)
84
+ }
85
+ beacon := newBeacon(t, serverPK)
86
+
87
+ regOut := registerBeaconWithInitial(t, server, beacon, nil)
88
+ phase2, err := beacon.ProcessInitialMessage(regOut.Serialized)
89
+ if err != nil {
90
+ t.Fatal(err)
91
+ }
92
+ if len(phase2) != 0 {
93
+ t.Fatalf("expected empty initial message, got %x", phase2)
94
+ }
95
+ }
96
+
72
97
  func TestEncryptToMultiple(t *testing.T) {
73
98
  server := newServer(t)
74
99
  serverPK, err := server.IdentityPK()
@@ -226,3 +251,116 @@ func TestDecryptCatchUp(t *testing.T) {
226
251
  t.Fatalf("catch-up decrypt mismatch: got %x and %x want %x", plain1, plain2, message)
227
252
  }
228
253
  }
254
+
255
+ func TestBeaconEncryptsToServer(t *testing.T) {
256
+ server := newServer(t)
257
+ serverPK, err := server.IdentityPK()
258
+ if err != nil {
259
+ t.Fatal(err)
260
+ }
261
+ beacon := newBeacon(t, serverPK)
262
+ message := []byte("beacon to server")
263
+
264
+ registerBeacon(t, server, beacon)
265
+ ciphertext, err := beacon.EncryptToServer(message)
266
+ if err != nil {
267
+ t.Fatal(err)
268
+ }
269
+ plaintext, err := server.DecryptBeaconMessage(1, ciphertext)
270
+ if err != nil {
271
+ t.Fatal(err)
272
+ }
273
+ if !bytes.Equal(plaintext, message) {
274
+ t.Fatalf("beacon-to-server decrypt mismatch: got %x want %x", plaintext, message)
275
+ }
276
+ }
277
+
278
+ func TestBeaconEncryptsToServerSigned(t *testing.T) {
279
+ server := newServer(t)
280
+ serverPK, err := server.IdentityPK()
281
+ if err != nil {
282
+ t.Fatal(err)
283
+ }
284
+ beacon := newBeacon(t, serverPK)
285
+ message := []byte("signed beacon to server")
286
+
287
+ registerBeacon(t, server, beacon)
288
+ signed, err := beacon.EncryptToServerSigned(message)
289
+ if err != nil {
290
+ t.Fatal(err)
291
+ }
292
+ plaintext, err := server.DecryptBeaconMessageSigned(signed)
293
+ if err != nil {
294
+ t.Fatal(err)
295
+ }
296
+ if !bytes.Equal(plaintext, message) {
297
+ t.Fatalf("signed beacon-to-server decrypt mismatch: got %x want %x", plaintext, message)
298
+ }
299
+ }
300
+
301
+ func TestSignedServerMessageRejectsTampering(t *testing.T) {
302
+ server := newServer(t)
303
+ serverPK, err := server.IdentityPK()
304
+ if err != nil {
305
+ t.Fatal(err)
306
+ }
307
+ beacon := newBeacon(t, serverPK)
308
+
309
+ registerBeacon(t, server, beacon)
310
+ signed, err := server.EncryptToBeaconSigned(1, []byte("server to beacon"))
311
+ if err != nil {
312
+ t.Fatal(err)
313
+ }
314
+ signed[len(signed)-1] ^= 0x01
315
+
316
+ if _, err := beacon.DecryptServerMessageSigned(signed); err == nil {
317
+ t.Fatal("expected tampered signed message to be rejected")
318
+ }
319
+ }
320
+
321
+ func TestBeaconCannotDecryptMessageForDifferentBeacon(t *testing.T) {
322
+ server := newServer(t)
323
+ serverPK, err := server.IdentityPK()
324
+ if err != nil {
325
+ t.Fatal(err)
326
+ }
327
+ b1 := newBeacon(t, serverPK)
328
+ b2 := newBeacon(t, serverPK)
329
+
330
+ registerBeacon(t, server, b1)
331
+ registerBeacon(t, server, b2)
332
+ ciphertext, err := server.EncryptToBeacon(1, []byte("for b1 only"))
333
+ if err != nil {
334
+ t.Fatal(err)
335
+ }
336
+
337
+ if _, err := b2.DecryptServerMessage(ciphertext); err == nil {
338
+ t.Fatal("expected message for another beacon to be rejected")
339
+ }
340
+ }
341
+
342
+ func TestCiphertextCannotBeReplayed(t *testing.T) {
343
+ server := newServer(t)
344
+ serverPK, err := server.IdentityPK()
345
+ if err != nil {
346
+ t.Fatal(err)
347
+ }
348
+ beacon := newBeacon(t, serverPK)
349
+ message := []byte("one shot")
350
+
351
+ registerBeacon(t, server, beacon)
352
+ ciphertext, err := server.EncryptToBeacon(1, message)
353
+ if err != nil {
354
+ t.Fatal(err)
355
+ }
356
+ plaintext, err := beacon.DecryptServerMessage(ciphertext)
357
+ if err != nil {
358
+ t.Fatal(err)
359
+ }
360
+ if !bytes.Equal(plaintext, message) {
361
+ t.Fatalf("replay setup decrypt mismatch: got %x want %x", plaintext, message)
362
+ }
363
+ if _, err := beacon.DecryptServerMessage(ciphertext); err == nil {
364
+ t.Fatal("expected replayed ciphertext to be rejected")
365
+ }
366
+ }
@@ -455,10 +455,18 @@ struct beaconcrypt_GoBuffer beaconcrypt_go_decrypt_beacon_message(struct beaconc
455
455
  const uint8_t *ptr,
456
456
  uintptr_t len);
457
457
 
458
+ struct beaconcrypt_GoBuffer beaconcrypt_go_decrypt_beacon_message_signed(struct beaconcrypt_BeaconCryptPqxdh *handle,
459
+ const uint8_t *ptr,
460
+ uintptr_t len);
461
+
458
462
  struct beaconcrypt_GoBuffer beaconcrypt_go_encrypt_to_server(struct beaconcrypt_BeaconCryptPqxdh *handle,
459
463
  const uint8_t *ptr,
460
464
  uintptr_t len);
461
465
 
466
+ struct beaconcrypt_GoBuffer beaconcrypt_go_encrypt_to_server_signed(struct beaconcrypt_BeaconCryptPqxdh *handle,
467
+ const uint8_t *ptr,
468
+ uintptr_t len);
469
+
462
470
  struct beaconcrypt_GoBuffer beaconcrypt_go_decrypt_server_message(struct beaconcrypt_BeaconCryptPqxdh *handle,
463
471
  const uint8_t *ptr,
464
472
  uintptr_t len);
@@ -320,6 +320,7 @@ impl ProviderBeacon for BeaconCryptCnsa2 {
320
320
  self.init_ratchets(&derived_secret, &info_str, true, srv_key_id);
321
321
 
322
322
  match response.get_app_cipher_text() {
323
+ Ok(ciphertext) if ciphertext.is_empty() => Some(vec![]),
323
324
  Ok(ciphertext) => match self.decrypt_message(ciphertext, srv_key_id, true) {
324
325
  Some(plaintext) => Some(plaintext),
325
326
  None => None,
@@ -296,6 +296,28 @@ mod gobinds {
296
296
  decrypt(handle, ptr, len, key_id, false)
297
297
  }
298
298
 
299
+ #[unsafe(no_mangle)]
300
+ pub extern "C" fn beaconcrypt_go_decrypt_beacon_message_signed(
301
+ handle: *mut BeaconCryptPqxdh,
302
+ ptr: *const u8,
303
+ len: usize,
304
+ ) -> GoBuffer {
305
+ if handle.is_null() {
306
+ return empty_buffer();
307
+ }
308
+ let Some(data) = (unsafe { input(ptr, len) }) else {
309
+ return empty_buffer();
310
+ };
311
+ let provider = unsafe { &mut *handle };
312
+ match provider.verify_signature(data) {
313
+ Some(verified) => provider
314
+ .decrypt_message(&verified.data, verified.key_id, false)
315
+ .map(into_buffer)
316
+ .unwrap_or_else(empty_buffer),
317
+ None => empty_buffer(),
318
+ }
319
+ }
320
+
299
321
  #[unsafe(no_mangle)]
300
322
  pub extern "C" fn beaconcrypt_go_encrypt_to_server(
301
323
  handle: *mut BeaconCryptPqxdh,
@@ -309,6 +331,29 @@ mod gobinds {
309
331
  encrypt(handle, ptr, len, false, provider.server_kid())
310
332
  }
311
333
 
334
+ #[unsafe(no_mangle)]
335
+ pub extern "C" fn beaconcrypt_go_encrypt_to_server_signed(
336
+ handle: *mut BeaconCryptPqxdh,
337
+ ptr: *const u8,
338
+ len: usize,
339
+ ) -> GoBuffer {
340
+ if handle.is_null() {
341
+ return empty_buffer();
342
+ }
343
+ let Some(data) = (unsafe { input(ptr, len) }) else {
344
+ return empty_buffer();
345
+ };
346
+ let provider = unsafe { &mut *handle };
347
+ let srv_kid = provider.server_kid();
348
+ match provider.encrypt_message(data, false, srv_kid) {
349
+ Some(ciphertext) => provider
350
+ .sign_message(ciphertext.as_slice())
351
+ .map(into_buffer)
352
+ .unwrap_or_else(empty_buffer),
353
+ None => empty_buffer(),
354
+ }
355
+ }
356
+
312
357
  #[unsafe(no_mangle)]
313
358
  pub extern "C" fn beaconcrypt_go_decrypt_server_message(
314
359
  handle: *mut BeaconCryptPqxdh,
@@ -365,6 +365,7 @@ impl ProviderBeacon for BeaconCryptPqxdh {
365
365
  self.init_ratchets(&derived_secret, &info_str, true, srv_key_id);
366
366
 
367
367
  match response.get_app_cipher_text() {
368
+ Ok(ciphertext) if ciphertext.is_empty() => Some(vec![]),
368
369
  Ok(ciphertext) => self.decrypt_message(ciphertext, srv_key_id, true),
369
370
  Err(_) => Some(vec![0u8; 0]),
370
371
  }
@@ -0,0 +1,119 @@
1
+ use beaconcrypt::*;
2
+
3
+ const SERVER_KID: u64 = 0;
4
+
5
+ fn new_pair() -> (BeaconCryptPqxdh, BeaconCryptPqxdh) {
6
+ let server = BeaconCryptPqxdh::new(false, SERVER_KID, None, None);
7
+ let server_id = server.identity_pk().to_owned();
8
+ let beacon = BeaconCryptPqxdh::new(true, SERVER_KID, Some(server_id.as_bytes()), None);
9
+ (server, beacon)
10
+ }
11
+
12
+ fn register_beacon(
13
+ server: &mut BeaconCryptPqxdh,
14
+ beacon: &mut BeaconCryptPqxdh,
15
+ initial_message: Option<&[u8]>,
16
+ ) -> RegResponse {
17
+ let phase_1 = beacon.get_registration_bundle().unwrap();
18
+ let reg_out = server.get_shared_secret(&phase_1).unwrap();
19
+ let phase_2 = server
20
+ .build_registration_response(reg_out, initial_message)
21
+ .unwrap();
22
+ let plaintext = beacon.finish_registration(&phase_2.serialized).unwrap();
23
+ assert_eq!(plaintext, initial_message.unwrap_or(&[]));
24
+ phase_2
25
+ }
26
+
27
+ #[test]
28
+ fn registration_can_omit_initial_message() {
29
+ let (mut server, mut beacon) = new_pair();
30
+
31
+ let response = register_beacon(&mut server, &mut beacon, None);
32
+
33
+ assert_eq!(response.kid, 1);
34
+ }
35
+
36
+ #[test]
37
+ fn beacon_can_encrypt_to_server() {
38
+ let (mut server, mut beacon) = new_pair();
39
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
40
+ let message = b"beacon to server";
41
+
42
+ let ciphertext = beacon
43
+ .encrypt_message(message.as_slice(), false, SERVER_KID)
44
+ .unwrap();
45
+ let plaintext = server
46
+ .decrypt_message(&ciphertext, response.kid, false)
47
+ .unwrap();
48
+
49
+ assert_eq!(plaintext, message);
50
+ }
51
+
52
+ #[test]
53
+ fn beacon_can_encrypt_to_server_signed() {
54
+ let (mut server, mut beacon) = new_pair();
55
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
56
+ let message = b"signed beacon to server";
57
+
58
+ let ciphertext = beacon
59
+ .encrypt_message(message.as_slice(), false, SERVER_KID)
60
+ .unwrap();
61
+ let signed = beacon.sign_message(&ciphertext).unwrap();
62
+ let verified = server.verify_signature(&signed).unwrap();
63
+ let plaintext = server
64
+ .decrypt_message(&verified.data, verified.key_id, false)
65
+ .unwrap();
66
+
67
+ assert_eq!(verified.key_id, response.kid);
68
+ assert_eq!(plaintext, message);
69
+ }
70
+
71
+ #[test]
72
+ fn signed_server_message_rejects_tampering() {
73
+ let (mut server, mut beacon) = new_pair();
74
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
75
+
76
+ let ciphertext = server
77
+ .encrypt_message(b"server to beacon", true, response.kid)
78
+ .unwrap();
79
+ let mut signed = server.sign_message(&ciphertext).unwrap();
80
+ let last = signed.len() - 1;
81
+ signed[last] ^= 0x01;
82
+
83
+ assert!(beacon.verify_signature(&signed).is_none());
84
+ }
85
+
86
+ #[test]
87
+ fn beacon_cannot_decrypt_message_for_different_beacon() {
88
+ let mut server = BeaconCryptPqxdh::new(false, SERVER_KID, None, None);
89
+ let server_id = server.identity_pk().to_owned();
90
+ let mut b1 = BeaconCryptPqxdh::new(true, SERVER_KID, Some(server_id.as_bytes()), None);
91
+ let mut b2 = BeaconCryptPqxdh::new(true, SERVER_KID, Some(server_id.as_bytes()), None);
92
+ let b1_response = register_beacon(&mut server, &mut b1, Some(&[0xFF; 32]));
93
+ let _ = register_beacon(&mut server, &mut b2, Some(&[0xFF; 32]));
94
+
95
+ let ciphertext = server
96
+ .encrypt_message(b"for b1 only", true, b1_response.kid)
97
+ .unwrap();
98
+
99
+ assert!(b2.decrypt_message(&ciphertext, SERVER_KID, true).is_none());
100
+ }
101
+
102
+ #[test]
103
+ fn ciphertext_cannot_be_replayed() {
104
+ let (mut server, mut beacon) = new_pair();
105
+ let response = register_beacon(&mut server, &mut beacon, Some(&[0xFF; 32]));
106
+ let message = b"one shot";
107
+
108
+ let ciphertext = server.encrypt_message(message, true, response.kid).unwrap();
109
+ let first = beacon
110
+ .decrypt_message(&ciphertext, SERVER_KID, true)
111
+ .unwrap();
112
+
113
+ assert_eq!(first, message);
114
+ assert!(
115
+ beacon
116
+ .decrypt_message(&ciphertext, SERVER_KID, true)
117
+ .is_none()
118
+ );
119
+ }
@@ -5,7 +5,7 @@ def register_beacon(
5
5
  server: BeaconCryptServer,
6
6
  beacon: BeaconCryptBeacon,
7
7
  ) -> bytes | None:
8
- message = bytes(0xFF * 32)
8
+ message = bytes([0xFF]) * 32
9
9
  phase_1 = beacon.generate_registration()
10
10
  assert phase_1 is not None
11
11
  reg_out = server.register_beacon(phase_1, message)
@@ -21,7 +21,7 @@ def test_encrypt_to_multiple():
21
21
  server_pk = server.id_pk()
22
22
  b1 = BeaconCryptBeacon(0, server_pk)
23
23
  b2 = BeaconCryptBeacon(0, server_pk)
24
- message = bytes(0x1 * 32)
24
+ message = bytes([0x1]) * 32
25
25
 
26
26
  b1_initial = register_beacon(server, b1)
27
27
  b2_initial = register_beacon(server, b2)
@@ -36,7 +36,7 @@ def test_encrypt_multiple():
36
36
  server = BeaconCryptServer(0, None)
37
37
  server_pk = server.id_pk()
38
38
  b1 = BeaconCryptBeacon(0, server_pk)
39
- message = bytes(0x1 * 32)
39
+ message = bytes([0x1]) * 32
40
40
 
41
41
  _ = register_beacon(server, b1)
42
42
 
@@ -49,7 +49,7 @@ def test_decrypt_multiple():
49
49
  server = BeaconCryptServer(0, None)
50
50
  server_pk = server.id_pk()
51
51
  beacon = BeaconCryptBeacon(0, server_pk)
52
- message = bytes(0x1 * 32)
52
+ message = bytes([0x1]) * 32
53
53
 
54
54
  _ = register_beacon(server, beacon)
55
55
  m1 = server.encrypt_to_beacon(message, 1)
@@ -66,7 +66,7 @@ def test_decrypt_multiple_signed():
66
66
  server = BeaconCryptServer(0, None)
67
67
  server_pk = server.id_pk()
68
68
  beacon = BeaconCryptBeacon(0, server_pk)
69
- message = bytes(0x1 * 32)
69
+ message = bytes([0x1]) * 32
70
70
 
71
71
  _ = register_beacon(server, beacon)
72
72
  m1 = server.encrypt_to_beacon_signed(message, 1)
@@ -83,7 +83,7 @@ def test_decrypt_catch_up():
83
83
  server = BeaconCryptServer(0, None)
84
84
  server_pk = server.id_pk()
85
85
  beacon = BeaconCryptBeacon(0, server_pk)
86
- message = bytes(0x1 * 32)
86
+ message = bytes([0x1]) * 32
87
87
 
88
88
  _ = register_beacon(server, beacon)
89
89
  m1 = server.encrypt_to_beacon(message, 1)
@@ -94,3 +94,75 @@ def test_decrypt_catch_up():
94
94
  plain1 = beacon.decrypt_server_message(m1)
95
95
  assert plain2 is not None and plain1 is not None
96
96
  assert plain2 == plain1 == message
97
+
98
+
99
+ def test_beacon_encrypts_to_server():
100
+ server = BeaconCryptServer(0, None)
101
+ server_pk = server.id_pk()
102
+ beacon = BeaconCryptBeacon(0, server_pk)
103
+ message = b"beacon to server"
104
+
105
+ _ = register_beacon(server, beacon)
106
+ ciphertext = beacon.encrypt_message_to_server(message)
107
+ assert ciphertext is not None
108
+
109
+ plaintext = server.decrypt_beacon_message(ciphertext, 1)
110
+ assert plaintext == message
111
+
112
+
113
+ def test_beacon_encrypts_to_server_signed():
114
+ server = BeaconCryptServer(0, None)
115
+ server_pk = server.id_pk()
116
+ beacon = BeaconCryptBeacon(0, server_pk)
117
+ message = b"signed beacon to server"
118
+
119
+ _ = register_beacon(server, beacon)
120
+ signed = beacon.encrypt_to_server_signed(message)
121
+ assert signed is not None
122
+
123
+ plaintext = server.decrypt_beacon_message_signed(signed)
124
+ assert plaintext == message
125
+
126
+
127
+ def test_signed_server_message_rejects_tampering():
128
+ server = BeaconCryptServer(0, None)
129
+ server_pk = server.id_pk()
130
+ beacon = BeaconCryptBeacon(0, server_pk)
131
+ message = b"server to beacon"
132
+
133
+ _ = register_beacon(server, beacon)
134
+ signed = server.encrypt_to_beacon_signed(message, 1)
135
+ assert signed is not None
136
+ tampered = bytearray(signed)
137
+ tampered[-1] ^= 0x01
138
+
139
+ assert beacon.decrypt_server_message_signed(bytes(tampered)) is None
140
+
141
+
142
+ def test_beacon_cannot_decrypt_message_for_different_beacon():
143
+ server = BeaconCryptServer(0, None)
144
+ server_pk = server.id_pk()
145
+ b1 = BeaconCryptBeacon(0, server_pk)
146
+ b2 = BeaconCryptBeacon(0, server_pk)
147
+ message = b"for b1 only"
148
+
149
+ _ = register_beacon(server, b1)
150
+ _ = register_beacon(server, b2)
151
+ ciphertext = server.encrypt_to_beacon(message, 1)
152
+ assert ciphertext is not None
153
+
154
+ assert b2.decrypt_server_message(ciphertext) is None
155
+
156
+
157
+ def test_ciphertext_cannot_be_replayed():
158
+ server = BeaconCryptServer(0, None)
159
+ server_pk = server.id_pk()
160
+ beacon = BeaconCryptBeacon(0, server_pk)
161
+ message = b"one shot"
162
+
163
+ _ = register_beacon(server, beacon)
164
+ ciphertext = server.encrypt_to_beacon(message, 1)
165
+ assert ciphertext is not None
166
+
167
+ assert beacon.decrypt_server_message(ciphertext) == message
168
+ assert beacon.decrypt_server_message(ciphertext) is None
@@ -5,7 +5,7 @@ def register_beacon(
5
5
  server: BeaconCryptServer,
6
6
  beacon: BeaconCryptBeacon,
7
7
  ) -> bytes | None:
8
- message = bytes(0xFF * 32)
8
+ message = bytes([0xFF]) * 32
9
9
  phase_1 = beacon.generate_registration()
10
10
  assert phase_1 is not None
11
11
  reg_out = server.register_beacon(phase_1, message)
@@ -22,3 +22,17 @@ def test_register():
22
22
  beacon = BeaconCryptBeacon(0, server_pk)
23
23
 
24
24
  assert register_beacon(server, beacon) is not None
25
+
26
+
27
+ def test_register_without_initial_message():
28
+ server = BeaconCryptServer(0, None)
29
+ server_pk = server.id_pk()
30
+ beacon = BeaconCryptBeacon(0, server_pk)
31
+
32
+ phase_1 = beacon.generate_registration()
33
+ assert phase_1 is not None
34
+ reg_out = server.register_beacon(phase_1, None)
35
+ assert reg_out is not None
36
+
37
+ phase_2 = beacon.process_initial_message(reg_out.serialized())
38
+ assert phase_2 == b""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes