From aaec3341e832301bec03e98e69aeaa0b9015bdbe Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Sun, 13 Oct 2024 10:05:54 -0600 Subject: [PATCH 1/7] some documentation things --- crates/o5/README.md | 21 +- doc/pq-obfs-with-extra-data.md | 445 +++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+), 10 deletions(-) create mode 100644 doc/pq-obfs-with-extra-data.md diff --git a/crates/o5/README.md b/crates/o5/README.md index da76dca..a87efc9 100644 --- a/crates/o5/README.md +++ b/crates/o5/README.md @@ -18,29 +18,30 @@ date elements without worrying about being backward compatible. - the concept of "packets" is now called "messages" - a frame can contain multiple messages - update from xsalsa20poly1305 -> chacha20poly1305 - - padding is given an explicit message type different than that of a payload and uses the mesage length header field + - padding is given an explicit message type different than that of a payload message and uses the mesage length header field - (In obfs4 a frame that decodes to a payload packet type `\x00` with packet length 0 is asummed to all be padding) - move payload to message type `\x01` - padding takes message type `\x00` - (Maybe) add bidirectional heartbeat messages + - Handshake - - x25519 key-exchange -> Kyber1024X25519 key-exchange + - x25519 key-exchange -> [X-Wing](https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem#name-with-hpke-x25519kyber768dra) (ML-KEM + X25519) Hybrid Public Key Exchange - the overhead padding of the current obfs4 handshake (resulting in paket length in [4096:8192]) is mostly unused we exchange some of this unused padding for a kyber key to provide post-quantum security to the handshake. - - Are Kyber1024 keys uniform random? I assume not. - - NTor V3 handshake - - the obfs4 handshake uses (a custom version of) the ntor handshake to derive key materials - - (Maybe) change mark and MAC from sha256-128 to sha256 - - handshake parameters encrypted under the key exchange public keys + - [Kemeleon Encoding](https://docs.rs/kemeleon/latest/kemeleon/) for obfuscating ML-KEM public keys and ciphertext on the wire. + - [Elligator2](https://docs.rs/curve25519-elligator2/latest/curve25519_elligator2/) for obfuscating X25519 public keys. + - [PQ-Obfs handshake](https://eprint.iacr.org/2024/1086.pdf) + - Adapted from the [NTor V3 handshake](https://spec.torproject.org/proposals/332-ntor-v3-with-extra-data.html) + - Change mark and MAC from sha256-128 to sha3-256 + - Allow messages extra data to be sent with the handshake, encrypted under the key exchange public keys - the client can provide initial parameters during the handshake, knowing that they are not forward secure. - the server can provide messages with parameters / extensions in the handshake response (like prngseed) - - like the kyber key, this takes space out of the padding already used in the client handshake. + - This takes space out of the padding already used in the client handshake. - (Maybe) session tickets and resumption - - (Maybe) handshake complete frame type ### Goals +* Post Quantum Forward Secrecy - traffic captured today cannot be decrypted by a quantum computer tomorrow. * Stick closer to Codec / Framed implementation for all packets (hadshake included) -* use the tor/arti ntor v3 implementation ### Features to keep - once a session is established, unrecognized frame types are ignored diff --git a/doc/pq-obfs-with-extra-data.md b/doc/pq-obfs-with-extra-data.md new file mode 100644 index 0000000..d53da31 --- /dev/null +++ b/doc/pq-obfs-with-extra-data.md @@ -0,0 +1,445 @@ + +# TODO: THIS IS FOR LAYOUT AND HAS NOT BEEN UPDATED + +changes still to come + +``` +Filename: 332-ntor-v3-with-extra-data.md +Title: Ntor protocol with extra data, version 3. +Author: Nick Mathewson +Created: 12 July 2021 +Status: Closed +``` + +# Overview + +The ntor handshake is our current protocol for circuit +establishment. + +So far we have two variants of the ntor handshake in use: the "ntor +v1" that we use for everyday circuit extension (see `tor-spec.txt`) +and the "hs-ntor" that we use for v3 onion service handshake (see +`rend-spec-v3.txt`). This document defines a third version of ntor, +adapting the improvements from hs-ntor for use in regular circuit +establishment. + +These improvements include: + + * Support for sending additional encrypted and authenticated + protocol-setup handshake data as part of the ntor handshake. (The + information sent from the client to the relay does not receive + forward secrecy.) + + * Support for using an external shared secret that both parties must + know in order to complete the handshake. (In the HS handshake, this + is the subcredential. We don't use it for circuit extension, but in + theory we could.) + + * Providing a single specification that can, in the future, be used + both for circuit extension _and_ HS introduction. + +# The improved protocol: an abstract view + +Given a client "C" that wants to construct a circuit to a +relay "S": + +The client knows: + * B: a public "onion key" for S + * ID: an identity for S, represented as a fixed-length + byte string. + * CM: a message that it wants to send to S as part of the + handshake. + * An optional "verification" string. + +The relay knows: + * A set of \[(b,B)...\] "onion key" keypairs. One of them is + "current", the others are outdated, but still valid. + * ID: Its own identity. + * A function for computing a server message SM, based on a given + client message. + * An optional "verification" string. This must match the "verification" + string from the client. + +Both parties have a strong source of randomness. + +Given this information, the client computes a "client handshake" +and sends it to the relay. + +The relay then uses its information plus the client handshake to see +if the incoming message is valid; if it is, then it computes a +"server handshake" to send in reply. + +The client processes the server handshake, and either succeeds or fails. + +At this point, the client and the relay both have access to: + * CM (the message the client sent) + * SM (the message the relay sent) + * KS (a shared byte stream of arbitrary length, used to compute + keys to be used elsewhere in the protocol). + +Additionally, the client knows that CM was sent _only_ to the relay +whose public onion key is B, and that KS is shared _only_ with that +relay. + +The relay does not know which client participated in the handshake, +but it does know that CM came from the same client that generated +the key X, and that SM and KS were shared _only_ with that client. + +Both parties know that CM, SM, and KS were shared correctly, or not +at all. + +Both parties know that they used the same verification string; if +they did not, they do not learn what the verification string was. +(This feature is required for HS handshakes.) + +# The handshake in detail + +## Notation + +We use the following notation: + + * `|` -- concatenation + * `"..."` -- a byte string, with no terminating NUL. + * `ENCAP(s)` -- an encapsulation function. We define this + as `htonll(len(s)) | s`. (Note that `len(ENCAP(s)) = len(s) + 8`). + * `PARTITION(s, n1, n2, n3, ...)` -- a function that partitions a + bytestring `s` into chunks of length `n1`, `n2`, `n3`, and so + on. Extra data is put into a final chunk. If `s` is not long + enough, the function fails. + +We require the following crypto operations: + + * `KDF(s,t)` -- a tweakable key derivation function, returning a + keystream of arbitrary length. + * `H(s,t)` -- a tweakable hash function of output length + `DIGEST_LEN`. + * `MAC(k, msg, t)` -- a tweakable message-authentication-code function, + with key length `MAC_KEY_LEN` and output length `MAC_LEN`. + * `EXP(pk,sk)` -- our Diffie Hellman group operation, taking a + public key of length `PUB_KEY_LEN`. + * `KEYGEN()` -- our Diffie-Hellman keypair generation algorithm, + returning a (secret-key,public-key) pair. + * `ENC(k, m)` -- a stream cipher with key of length `ENC_KEY_LEN`. + `DEC(k, m)` is its inverse. + +Parameters: + + * `PROTOID` -- a short protocol identifier + * `t_*` -- a set of "tweak" strings, used to derive distinct + hashes from a single hash function. + * `ID_LEN` -- the length of an identity key that uniquely identifies + a relay. + +Given our cryptographic operations and a set of tweak strings, we +define: + +``` +H_foo(s) = H(s, t_foo) +MAC_foo(k, msg) = MAC(k, msg, t_foo) +KDF_foo(s) = KDF(s, t_foo) +``` + +See Appendix A.1 below for a set of instantiations for these operations +and constants. + +## Client operation, phase 1 + +The client knows: + B, ID -- the onion key and ID of the relay it wants to use. + CM -- the message that it wants to send as part of its + handshake. + VER -- a verification string. + +First, the client generates a single-use keypair: + + x,X = KEYGEN() + +and computes: + + Bx = EXP(B,x) + secret_input_phase1 = Bx | ID | X | B | PROTOID | ENCAP(VER) + phase1_keys = KDF_msgkdf(secret_input_phase1) + (ENC_K1, MAC_K1) = PARTITION(phase1_keys, ENC_KEY_LEN, MAC_KEY_LEN) + + encrypted_msg = ENC(ENC_K1, CM) + msg_mac = MAC_msgmac(MAC_K1, ID | B | X | encrypted_msg) + +and sends: + + NODEID ID [ID_LEN bytes] + KEYID B [PUB_KEY_LEN bytes] + CLIENT_PK X [PUB_KEY_LEN bytes] + MSG encrypted_msg [len(CM) bytes] + MAC msg_mac [last MAC_LEN bytes of message] + +The client remembers x, X, B, ID, Bx, and msg_mac. + +## Server operation + +The relay checks whether NODEID is as expected, and looks up +the (b,B) keypair corresponding to KEYID. If the keypair is +missing or the NODEID is wrong, the handshake fails. + +Now the relay uses `X=CLIENT_PK` to compute: + + Xb = EXP(X,b) + secret_input_phase1 = Xb | ID | X | B | PROTOID | ENCAP(VER) + phase1_keys = KDF_msgkdf(secret_input_phase1) + (ENC_K1, MAC_K1) = PARTITION(phase1_keys, ENC_KEY_LEN, MAC_KEY_LEN) + + expected_mac = MAC_msgmac(MAC_K1, ID | B | X | MSG) + +If `expected_mac` is not `MAC`, the handshake fails. Otherwise +the relay computes `CM` as: + + CM = DEC(MSG, ENC_K1) + +The relay then checks whether `CM` is well-formed, and in response +composes `SM`, the reply that it wants to send as part of the +handshake. It then generates a new ephemeral keypair: + + y,Y = KEYGEN() + +and computes the rest of the handshake: + + Xy = EXP(X,y) + secret_input = Xy | Xb | ID | B | X | Y | PROTOID | ENCAP(VER) + ntor_key_seed = H_key_seed(secret_input) + verify = H_verify(secret_input) + + RAW_KEYSTREAM = KDF_final(ntor_key_seed) + (ENC_KEY, KEYSTREAM) = PARTITION(RAW_KEYSTREAM, ENC_KEY_LKEN, ...) + + encrypted_msg = ENC(ENC_KEY, SM) + + auth_input = verify | ID | B | Y | X | MAC | ENCAP(encrypted_msg) | + PROTOID | "Server" + AUTH = H_auth(auth_input) + +The relay then sends: + + Y Y [PUB_KEY_LEN bytes] + AUTH AUTH [DIGEST_LEN bytes] + MSG encrypted_msg [len(SM) bytes, up to end of the message] + +The relay uses KEYSTREAM to generate the shared secrets for the +newly created circuit. + +## Client operation, phase 2 + +The client computes: + + Yx = EXP(Y, x) + secret_input = Yx | Bx | ID | B | X | Y | PROTOID | ENCAP(VER) + ntor_key_seed = H_key_seed(secret_input) + verify = H_verify(secret_input) + + auth_input = verify | ID | B | Y | X | MAC | ENCAP(MSG) | + PROTOID | "Server" + AUTH_expected = H_auth(auth_input) + +If AUTH_expected is equal to AUTH, then the handshake has +succeeded. The client can then calculate: + + RAW_KEYSTREAM = KDF_final(ntor_key_seed) + (ENC_KEY, KEYSTREAM) = PARTITION(RAW_KEYSTREAM, ENC_KEY_LKEN, ...) + + SM = DEC(ENC_KEY, MSG) + +SM is the message from the relay, and the client uses KEYSTREAM to +generate the shared secrets for the newly created circuit. + +# Security notes + +Whenever comparing bytestrings, implementations SHOULD use +constant-time comparison function to avoid side-channel attacks. + +To avoid small-subgroup attacks against the Diffie-Hellman function, +implementations SHOULD either: + + * Make sure that all incoming group members are in fact in the DH + group. + * Validate all outputs from the EXP function to make sure that + they are not degenerate. + + +# Notes on usage + +We don't specify what should actually be done with the resulting +keystreams; that depends on the usage for which this handshake is +employed. Typically, they'll be divided up into a series of tags +and symmetric keys. + +The keystreams generated here are (conceptually) unlimited. In +practice, the usage will determine the amount of key material +actually needed: that's the amount that clients and relays will +actually generate. + +The PROTOID parameter should be changed not only if the +cryptographic operations change here, but also if the usage changes +at all, or if the meaning of any parameters changes. (For example, +if the encoding of CM and SM changed, or if ID were a different +length or represented a different type of key, then we should start +using a new PROTOID.) + + +# A.1 Instantiation + +Here are a set of functions based on SHA3, SHAKE-256, Curve25519, and +AES256: + +``` +H(s, t) = SHA3_256(ENCAP(t) | s) +MAC(k, msg, t) = SHA3_256(ENCAP(t) | ENCAP(k) | s) +KDF(s, t) = SHAKE_256(ENCAP(t) | s) +ENC(k, m) = AES_256_CTR(k, m) + +EXP(pk,sk), KEYGEN: defined as in curve25519 + +DIGEST_LEN = MAC_LEN = MAC_KEY_LEN = ENC_KEY_LEN = PUB_KEY_LEN = 32 + +ID_LEN = 32 (representing an ed25519 identity key) +``` + +Notes on selected operations: SHA3 can be pretty slow, and AES256 is +likely overkill. I'm choosing them anyway because they are what we +use in hs-ntor, and in my preliminary experiments they don't account +for even 1% of the time spent on this handshake. + +``` +t_msgkdf = PROTOID | ":kdf_phase1" +t_msgmac = PROTOID | ":msg_mac" +t_key_seed = PROTOID | ":key_seed" +t_verify = PROTOID | ":verify" +t_final = PROTOID | ":kdf_final" +t_auth = PROTOID | ":auth_final" +``` + +# A.2 Encoding for use with Tor circuit extension + +Here we give a concrete instantiation of ntor-v3 for use with +circuit extension in Tor, and the parameters in A.1 above. + +If in use, this is a new CREATE2 type. Clients should not use it +unless the relay advertises support by including an appropriate +version of the `Relay=X` subprotocol in its protocols list. + +When the encoding and methods of this section, along with the +instantiations from the previous section, are in use, we specify: + + PROTOID = "ntor3-curve25519-sha3_256-1" + +The key material is extracted as follows, unless modified by the +handshake (see below). See tor-spec.txt for more info on the +specific values: + + Df Digest authentication, forwards [20 bytes] + Db Digest authentication, backwards [20 bytes] + Kf Encryption key, forwards [16 bytes] + Kb Encryption key, backwards [16 bytes] + KH Onion service nonce [20 bytes] + +We use the following meta-encoding for the contents of client and +server messages. + + [Any number of times]: + EXTENSION + EXT_FIELD_TYPE [one byte] + EXT_FIELD_LEN [one byte] + EXT_FIELD [EXT_FIELD_LEN bytes] + +(`EXT_FIELD_LEN` may be zero, in which case EXT_FIELD is absent.) + +All parties MUST reject messages that are not well-formed per the +rules above. + +We do not specify specific TYPE semantics here; we leave those for +other proposals and specifications. + +Parties MUST ignore extensions with `EXT_FIELD_TYPE` bodies they do not +recognize. + +Unless otherwise specified in the documentation for an extension type: +* Each extension type SHOULD be sent only once in a message. +* Parties MUST ignore any occurrences all occurrences of an extension + with a given type after the first such occurrence. +* Extensions SHOULD be sent in numerically ascending order by type. + +(The above extension sorting and multiplicity rules are only defaults; +they may be overridden in the description of individual extensions.) + +# A.3 How much space is available? + +We start with a 498-byte payload in each relay cell. + +The header of the EXTEND2 cell, including link specifiers and other +headers, comes to 89 bytes. + +The client handshake requires 128 bytes (excluding CM). + +That leaves 281 bytes, "which should be plenty". + +# X.1 Negotiating proposal-324 circuit windows + +(We should move this section into prop324 when this proposal is +finished.) + +We define a type value, CIRCWINDOW_INC. + +We define a triplet of consensus parameters: `circwindow_inc_min`, +`cincwindow_inc_max`, and `circwindow_inc_dflt`. These all have +range (1,65535). + +When the authority operators want to experiment with different +values for `circwindow_inc_dflt`, they set `circwindow_inc_min` and +`circwindow_inc_max` to the range in which they want to experiment, +making sure that the existing `circwindow_inc_dflt` is within that +range. + +vWhen a client sees that a relay supports the ntor3 handshake type +(subprotocol `Relay=X`), and also supports the flow control +algorithms of proposal 324 (subprotocol `FlowCtrl=X`), then the +client sends a message, with type `CIRCWINDOW_INC`, containing a +two-byte integer equal to `circwindow_inc_dflt`. + +The relay rejects the message if the value given is outside of the +\[`circwindow_inc_min`, `circwindow_inc_max`\] range. Otherwise, it +accepts it, and replies with the same message that the client sent. + +# X.2: Test vectors + +The following test values, in hex, were generated by a Python reference +implementation. + +Inputs: + +b = "4051daa5921cfa2a1c27b08451324919538e79e788a81b38cbed097a5dff454a" +B = "f8307a2bc1870b00b828bb74dbb8fd88e632a6375ab3bcd1ae706aaa8b6cdd1d" +ID = "9fad2af287ef942632833d21f946c6260c33fae6172b60006e86e4a6911753a2" +x = "b825a3719147bcbe5fb1d0b0fcb9c09e51948048e2e3283d2ab7b45b5ef38b49" +X = "252fe9ae91264c91d4ecb8501f79d0387e34ad8ca0f7c995184f7d11d5da4f46" +CM = "68656c6c6f20776f726c64" +VER = "78797a7a79" +y = "4865a5b7689dafd978f529291c7171bc159be076b92186405d13220b80e2a053" +Y = "4bf4814326fdab45ad5184f5518bd7fae25dc59374062698201a50a22954246d" +SM = "486f6c61204d756e646f" + +Intermediate values: + +ENC_K1 = "4cd166e93f1c60a29f8fb9ec40ea0fc878930c27800594593e1c4d0f3b5fbd02" +MAC_K1 = "f5b69e85fdd26e1b0bdbbc8128e32d8123040255f11f744af3cc98fc13613cda" +msg_mac = "9e044d53565f04d82bbb3bebed3d06cea65db8be9c72b68cd461942088502f67" +key_seed = "b9a092741098e1f5b8ab37ce74399dd57522c974d7ae4626283a1077b9273255" +verify = "1dc09fb249738a79f1bc3a545eee8c415f27213894a760bb4df58862e414799a" +ENC_KEY (server) = "cab8a93eef62246a83536c4384f331ec26061b66098c61421b6cae81f4f57c56" +AUTH = "2fc5f8773ca824542bc6cf6f57c7c29bbf4e5476461ab130c5b18ab0a9127665" + +Messages: + +client_handshake = "9fad2af287ef942632833d21f946c6260c33fae6172b60006e86e4a6911753a2f8307a2bc1870b00b828bb74dbb8fd88e632a6375ab3bcd1ae706aaa8b6cdd1d252fe9ae91264c91d4ecb8501f79d0387e34ad8ca0f7c995184f7d11d5da4f463bebd9151fd3b47c180abc9e044d53565f04d82bbb3bebed3d06cea65db8be9c72b68cd461942088502f67" + +server_handshake = "4bf4814326fdab45ad5184f5518bd7fae25dc59374062698201a50a22954246d2fc5f8773ca824542bc6cf6f57c7c29bbf4e5476461ab130c5b18ab0a91276651202c3e1e87c0d32054c" + +First 256 bytes of keystream: + +KEYSTREAM = "9c19b631fd94ed86a817e01f6c80b0743a43f5faebd39cfaa8b00fa8bcc65c3bfeaa403d91acbd68a821bf6ee8504602b094a254392a07737d5662768c7a9fb1b2814bb34780eaee6e867c773e28c212ead563e98a1cd5d5b4576f5ee61c59bde025ff2851bb19b721421694f263818e3531e43a9e4e3e2c661e2ad547d8984caa28ebecd3e4525452299be26b9185a20a90ce1eac20a91f2832d731b54502b09749b5a2a2949292f8cfcbeffb790c7790ed935a9d251e7e336148ea83b063a5618fcff674a44581585fd22077ca0e52c59a24347a38d1a1ceebddbf238541f226b8f88d0fb9c07a1bcd2ea764bbbb5dacdaf5312a14c0b9e4f06309b0333b4a" From 674f7f9604982bdf452e2065fdb69fc1c9b6a783 Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:02:13 -0600 Subject: [PATCH 2/7] isolating issues: working on compiling to focus on keys --- crates/o5/src/client.rs | 31 +- crates/o5/src/common/mod.rs | 2 +- crates/o5/src/common/ntor_arti.rs | 25 +- .../common/{mlkem1024_x25519.rs => xwing.rs} | 25 +- crates/o5/src/constants.rs | 6 +- crates/o5/src/framing/handshake.rs | 22 +- crates/o5/src/handshake.rs | 38 ++- crates/o5/src/handshake/client.rs | 270 ++++++++++-------- crates/o5/src/handshake/keys.rs | 25 +- crates/o5/src/handshake/server.rs | 228 +++++++-------- crates/o5/src/lib.rs | 1 + crates/o5/src/pt.rs | 15 +- crates/o5/src/server.rs | 5 +- crates/o5/src/sessions.rs | 8 +- crates/o5/src/test_utils/mod.rs | 1 + crates/o5/src/test_utils/test_keys.rs | 62 ++++ 16 files changed, 443 insertions(+), 321 deletions(-) rename crates/o5/src/common/{mlkem1024_x25519.rs => xwing.rs} (94%) create mode 100644 crates/o5/src/test_utils/test_keys.rs diff --git a/crates/o5/src/client.rs b/crates/o5/src/client.rs index 0dfb8cd..0497a0d 100644 --- a/crates/o5/src/client.rs +++ b/crates/o5/src/client.rs @@ -1,7 +1,7 @@ #![allow(unused)] use crate::{ - common::{colorize, mlkem1024_x25519, HmacSha256}, + common::{colorize, xwing, HmacSha256}, constants::*, framing::{FrameError, Marshall, O5Codec, TryParse, KEY_LENGTH, KEY_MATERIAL_LENGTH}, handshake::IdentityPublicKey, @@ -26,8 +26,7 @@ use std::{ #[derive(Clone, Debug)] pub struct ClientBuilder { - pub station_pubkey: [u8; PUBLIC_KEY_LEN], - pub station_id: [u8; NODE_ID_LENGTH], + pub node_details: IdentityPublicKey, pub statefile_path: Option, pub(crate) handshake_timeout: MaybeTimeout, } @@ -35,8 +34,8 @@ pub struct ClientBuilder { impl Default for ClientBuilder { fn default() -> Self { Self { - station_pubkey: [0u8; PUBLIC_KEY_LEN], - station_id: [0_u8; NODE_ID_LENGTH], + node_details: IdentityPublicKey::new([0u8; PUBLIC_KEY_LEN], [0u8; NODE_ID_LENGTH]) + .expect("default identitykey is broken - shouldn't be used anyways"), statefile_path: None, handshake_timeout: MaybeTimeout::Default_, } @@ -46,9 +45,9 @@ impl Default for ClientBuilder { impl ClientBuilder { /// TODO: implement client builder from statefile pub fn from_statefile(location: &str) -> Result { + todo!("this is not implemented"); Ok(Self { - station_pubkey: [0_u8; PUBLIC_KEY_LEN], - station_id: [0_u8; NODE_ID_LENGTH], + node_details: IdentityPublicKey::new([0u8; PUBLIC_KEY_LEN], [0u8; NODE_ID_LENGTH])?, statefile_path: Some(location.into()), handshake_timeout: MaybeTimeout::Default_, }) @@ -56,16 +55,21 @@ impl ClientBuilder { /// TODO: implement client builder from string args pub fn from_params(param_strs: Vec>) -> Result { + todo!("this is not implemented"); Ok(Self { - station_pubkey: [0_u8; PUBLIC_KEY_LEN], - station_id: [0_u8; NODE_ID_LENGTH], + node_details: IdentityPublicKey::new([0u8; PUBLIC_KEY_LEN], [0u8; NODE_ID_LENGTH])?, statefile_path: None, handshake_timeout: MaybeTimeout::Default_, }) } - pub fn with_node_pubkey(&mut self, pubkey: [u8; PUBLIC_KEY_LEN]) -> &mut Self { - self.station_pubkey = pubkey; + pub fn with_node_pubkey(&mut self, pubkey: [u8; PUBLIC_KEY_LEN]) -> Result<&mut Self> { + self.node_details.pk = xwing::PublicKey::try_from(&pubkey[..])?; + Ok(self) + } + + pub(crate) fn with_node(&mut self, pubkey: IdentityPublicKey) -> &mut Self { + self.node_details = pubkey; self } @@ -75,7 +79,7 @@ impl ClientBuilder { } pub fn with_node_id(&mut self, id: [u8; NODE_ID_LENGTH]) -> &mut Self { - self.station_id = id; + self.node_details.id = id.into(); self } @@ -96,8 +100,7 @@ impl ClientBuilder { pub fn build(&self) -> Client { Client { - station_pubkey: IdentityPublicKey::new(self.station_pubkey, self.station_id) - .expect("failed to build client - bad options."), + station_pubkey: self.node_details.clone(), handshake_timeout: self.handshake_timeout.duration(), } } diff --git a/crates/o5/src/common/mod.rs b/crates/o5/src/common/mod.rs index b3a1e9b..cd8c7d8 100644 --- a/crates/o5/src/common/mod.rs +++ b/crates/o5/src/common/mod.rs @@ -12,11 +12,11 @@ mod skip; pub use skip::discard; pub mod drbg; -pub mod mlkem1024_x25519; pub mod ntor_arti; pub mod probdist; pub mod replay_filter; pub mod x25519_elligator2; +pub mod xwing; pub trait ArgParse { type Output; diff --git a/crates/o5/src/common/ntor_arti.rs b/crates/o5/src/common/ntor_arti.rs index 897f429..35b6d01 100644 --- a/crates/o5/src/common/ntor_arti.rs +++ b/crates/o5/src/common/ntor_arti.rs @@ -13,10 +13,10 @@ use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use crate::{ - common::{colorize, mlkem1024_x25519}, + common::{colorize, xwing}, Error, Result, }; -//use zeroize::Zeroizing; + use tor_bytes::SecretBuf; pub const SESSION_ID_LEN: usize = 8; @@ -70,10 +70,8 @@ pub trait ClientHandshake { type HandshakeMaterials: ClientHandshakeMaterials; /// The type for the state that the client holds while waiting for a reply. type StateType; - /// A type that is returned and used to generate session keys.x - type KeyGen; /// Type of extra data returned by server (without forward secrecy). - type ServerAuxData; + type HsOutput: ClientHandshakeComplete; /// Generate a new client onionskin for a relay with a given onion key, /// including `client_aux_data` to be sent without forward secrecy. @@ -81,15 +79,22 @@ pub trait ClientHandshake { /// On success, return a state object that will be used to /// complete the handshake, along with the message to send. fn client1(materials: Self::HandshakeMaterials) -> Result<(Self::StateType, Vec)>; + /// Handle an onionskin from a relay, and produce aux data returned /// from the server, and a key generator. /// /// The state object must match the one that was used to make the /// client onionskin that the server is replying to. - fn client2>( - state: Self::StateType, - msg: T, - ) -> Result<(Self::ServerAuxData, Self::KeyGen)>; + fn client2>(state: Self::StateType, msg: T) -> Result<(Self::HsOutput)>; +} + +pub trait ClientHandshakeComplete { + type KeyGen; + type ServerAuxData; + type Remainder; + fn keygen(&self) -> &Self::KeyGen; + fn extensions(&self) -> &Self::ServerAuxData; + fn remainder(&self) -> &Self::Remainder; } /// Trait for an object that handles incoming auxiliary data and @@ -201,7 +206,7 @@ pub enum RelayHandshakeError { /// Error happened during cryptographic handshake #[error("")] - CryptoError(mlkem1024_x25519::EncodeError), + CryptoError(xwing::EncodeError), /// The client asked for a key we didn't have. #[error("Client asked for a key or ID that we don't have")] diff --git a/crates/o5/src/common/mlkem1024_x25519.rs b/crates/o5/src/common/xwing.rs similarity index 94% rename from crates/o5/src/common/mlkem1024_x25519.rs rename to crates/o5/src/common/xwing.rs index 414f1c5..1a57cfc 100644 --- a/crates/o5/src/common/mlkem1024_x25519.rs +++ b/crates/o5/src/common/xwing.rs @@ -116,23 +116,42 @@ impl From<&StaticSecret> for PublicKey { } } -impl TryFrom<[u8; PUBKEY_LEN]> for PublicKey { +impl TryFrom<&[u8]> for StaticSecret { type Error = Error; - fn try_from(value: [u8; PUBKEY_LEN]) -> Result { + fn try_from(value: &[u8]) -> Result { + StaticSecret::try_from_bytes(value) + } +} + +impl TryFrom<&[u8]> for PublicKey { + type Error = Error; + fn try_from(value: &[u8]) -> std::result::Result { + if value.len() < PUBKEY_LEN { + return Err(Error::Crypto("bad publickey".into())); + } let mut x25519 = [0u8; X25519_PUBKEY_LEN]; x25519.copy_from_slice(&value[..X25519_PUBKEY_LEN]); let mlkem = EncapsulationKey::try_from_bytes(&value[X25519_PUBKEY_LEN..]) .map_err(|e| Error::EncodeError(e.into()))?; + let mut pub_key = [0u8; PUBKEY_LEN]; + pub_key.copy_from_slice(&value[..PUBKEY_LEN]); Ok(Self { x25519: x25519.into(), mlkem, - pub_key: value, + pub_key, }) } } +impl TryFrom<[u8; PUBKEY_LEN]> for PublicKey { + type Error = Error; + fn try_from(value: [u8; PUBKEY_LEN]) -> Result { + Self::try_from(&value[..]) + } +} + pub struct SharedSecret { x25519: x25519_dalek::SharedSecret, x25519_raw: [u8; 32], diff --git a/crates/o5/src/constants.rs b/crates/o5/src/constants.rs index 9af94e8..b54488b 100644 --- a/crates/o5/src/constants.rs +++ b/crates/o5/src/constants.rs @@ -4,14 +4,14 @@ use tor_llcrypto::pk::ed25519::ED25519_ID_LEN; pub use crate::common::ntor_arti::SESSION_ID_LEN; use crate::{ - common::{drbg, mlkem1024_x25519, x25519_elligator2::REPRESENTATIVE_LENGTH}, + common::{drbg, x25519_elligator2::REPRESENTATIVE_LENGTH, xwing}, framing, handshake::AUTHCODE_LENGTH, }; use std::time::Duration; -pub const PUBLIC_KEY_LEN: usize = mlkem1024_x25519::PUBKEY_LEN; +pub const PUBLIC_KEY_LEN: usize = xwing::PUBKEY_LEN; //=========================[Framing/Msgs]=====================================// @@ -76,4 +76,4 @@ pub const SEED_LENGTH: usize = drbg::SEED_LENGTH; pub const HEADER_LENGTH: usize = framing::FRAME_OVERHEAD + framing::MESSAGE_OVERHEAD; pub const NODE_ID_LENGTH: usize = ED25519_ID_LEN; -pub const NODE_PUBKEY_LENGTH: usize = mlkem1024_x25519::PUBKEY_LEN; +pub const NODE_PUBKEY_LENGTH: usize = xwing::PUBKEY_LEN; diff --git a/crates/o5/src/framing/handshake.rs b/crates/o5/src/framing/handshake.rs index ed88886..1e68468 100644 --- a/crates/o5/src/framing/handshake.rs +++ b/crates/o5/src/framing/handshake.rs @@ -8,14 +8,17 @@ use crate::{ Result, }; -use bytes::BufMut; use block_buffer::Eager; -use digest::{core_api::{BlockSizeUser, CoreProxy, UpdateCore, FixedOutputCore, BufferKindUser}, HashMarker}; +use bytes::BufMut; +use digest::{ + core_api::{BlockSizeUser, BufferKindUser, CoreProxy, FixedOutputCore, UpdateCore}, + HashMarker, +}; use hmac::{Hmac, Mac}; use ptrs::trace; -use typenum::{consts::U256, operator_aliases::Le, type_operators::IsLess, marker_traits::NonZero}; use tor_cell::relaycell::extend::NtorV3Extension; use tor_llcrypto::d::Sha3_256; +use typenum::{consts::U256, marker_traits::NonZero, operator_aliases::Le, type_operators::IsLess}; // -----------------------------[ Server ]----------------------------- @@ -125,11 +128,7 @@ impl<'a> ClientHandshakeMessage<'a> { self.epoch_hour.clone() } - pub fn marshall( - &mut self, - buf: &mut impl BufMut, - key: &[u8], - ) -> Result<()> { + pub fn marshall(&mut self, buf: &mut impl BufMut, key: &[u8]) -> Result<()> { trace!("serializing client handshake"); let h = Hmac::::new_from_slice(key).unwrap(); @@ -140,7 +139,12 @@ impl<'a> ClientHandshakeMessage<'a> { pub fn marshall_inner(&mut self, _buf: &mut impl BufMut, _h: Hmac) -> Result<()> where D: CoreProxy, - D::Core: HashMarker + UpdateCore + FixedOutputCore + BufferKindUser + Default + Clone, + D::Core: HashMarker + + UpdateCore + + FixedOutputCore + + BufferKindUser + + Default + + Clone, ::BlockSize: IsLess, Le<::BlockSize, U256>: NonZero, { diff --git a/crates/o5/src/handshake.rs b/crates/o5/src/handshake.rs index 09c2ee0..8dda1c1 100644 --- a/crates/o5/src/handshake.rs +++ b/crates/o5/src/handshake.rs @@ -44,7 +44,7 @@ pub(crate) use client::{HandshakeMaterials as CHSMaterials, NtorV3Client}; mod server; pub(crate) use server::HandshakeMaterials as SHSMaterials; -use crate::common::mlkem1024_x25519; +use crate::common::xwing; /// The verification string to be used for circuit extension. pub const NTOR3_CIRC_VERIFICATION: &[u8] = b"circuit extend"; @@ -196,7 +196,7 @@ fn h_verify(d: &[u8]) -> DigestVal { /// diffie-hellman as Bx or Xb), the relay's public key information, /// the client's public key (B), and the shared verification string. fn kdf_msgkdf( - xb: &mlkem1024_x25519::SharedSecret, + xb: &xwing::SharedSecret, relay_public: &IdentityPublicKey, client_public: &SessionPublicKey, verification: &[u8], @@ -268,12 +268,13 @@ mod test { #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! - use crate::common::mlkem1024_x25519::{PublicKey, StaticSecret}; use crate::common::ntor_arti::{ClientHandshake, KeyGenerator, ServerHandshake}; + use crate::common::xwing::{PublicKey, StaticSecret}; use crate::constants::{NODE_ID_LENGTH, SEED_LENGTH}; use crate::Server; use super::*; + use crate::test_utils::test_keys::KEYS; use crate::{handshake::IdentitySecretKey, sessions::SessionSecretKey}; use hex::FromHex; @@ -308,7 +309,7 @@ mod test { &mut rng, &mut rep, &c_handshake, - &[relay_private], + &relay_private, verification, ) .unwrap(); @@ -389,25 +390,22 @@ mod test { #[test] fn test_ntor3_testvec() { let mut rng = rand::thread_rng(); - let b = hex!("4051daa5921cfa2a1c27b08451324919538e79e788a81b38cbed097a5dff454a"); - let id = <[u8; NODE_ID_LENGTH]>::from_hex( - "aaaaaaaaaaaaaaaaaaaaaaaa9fad2af287ef942632833d21f946c6260c33fae6", - ) - .unwrap(); - let x = hex!("b825a3719147bcbe5fb1d0b0fcb9c09e51948048e2e3283d2ab7b45b5ef38b49"); - let y = hex!("4865a5b7689dafd978f529291c7171bc159be076b92186405d13220b80e2a053"); - let b: StaticSecret = b.into(); - let B: PublicKey = (&b).into(); - let x: SessionSecretKey = x.into(); + let b = hex::decode(KEYS[0].b).expect("failed to unhex b"); + let id = <[u8; NODE_ID_LENGTH]>::from_hex(KEYS[0].id).unwrap(); + let x = hex::decode(KEYS[0].x).expect("failed to unhex x"); + let y = hex::decode(KEYS[0].y).expect("failed to unhex y"); + let b = StaticSecret::try_from(&b[..]).expect("failed to parse b"); + let B = PublicKey::from(&b); + let x = SessionSecretKey::try_from(&x[..]).expect("failed_to parse x"); //let X = (&x).into(); - let y: StaticSecret = y.into(); + let y = StaticSecret::try_from(&y[..]).expect("failed to parse y"); let client_message = hex!("68656c6c6f20776f726c64"); let verification = hex!("78797a7a79"); let server_message = hex!("486f6c61204d756e646f"); let relay_private = IdentitySecretKey::new(b, id.into()); - let relay_public = relay_private.pk; // { pk: B, id }; + let relay_public = IdentityPublicKey::from(&relay_private); // { pk: B, id }; let mut chs_materials = CHSMaterials::new(&relay_public, "".into()); let (state, client_handshake) = @@ -455,7 +453,7 @@ mod test { } #[test] - fn mlkem1024_x25519_3way_handshake_flow() { + fn xwing_3way_handshake_flow() { let mut rng = rand::thread_rng(); // long-term server id and keys let server_id_keys = StaticSecret::random_from_rng(&mut rng); @@ -465,17 +463,17 @@ mod test { // client open session, generating the associated ephemeral keys let client_session = StaticSecret::random_from_rng(&mut rng); - // client sends mlkem1024_x25519 session pubkey(s) + // client sends xwing session pubkey(s) let _cpk = PublicKey::from(&client_session); - // server computes mlkem1024_x25519 combined shared secret + // server computes xwing combined shared secret let _server_session = StaticSecret::random_from_rng(&mut rng); // let server_hs_res = server_handshake(&server_session, &cpk, &server_id_keys, &server_id); // server sends mlkemx25519 session pubkey(s) let _spk = PublicKey::from(&client_session); - // // client computes mlkem1024_x25519 combined shared secret + // // client computes xwing combined shared secret // let client_hs_res = client_handshake(&client_session, &spk, &server_id_pub, &server_id); // assert_ne!(client_hs_res.is_some().unwrap_u8(), 0); diff --git a/crates/o5/src/handshake/client.rs b/crates/o5/src/handshake/client.rs index 4d6673a..85b3d06 100644 --- a/crates/o5/src/handshake/client.rs +++ b/crates/o5/src/handshake/client.rs @@ -1,8 +1,8 @@ use crate::{ common::{ ct, - mlkem1024_x25519::SharedSecret, - ntor_arti::{ClientHandshake, ClientHandshakeMaterials}, + ntor_arti::{ClientHandshake, ClientHandshakeComplete, ClientHandshakeMaterials}, + xwing::SharedSecret, }, constants::*, framing::handshake::ClientHandshakeMessage, @@ -26,7 +26,6 @@ use tor_llcrypto::{ }; use zeroize::Zeroizing; - /// Client state for the o5 (ntor v3) handshake. /// /// The client needs to hold this state between when it sends its part @@ -52,7 +51,7 @@ pub(crate) struct HandshakeState { } impl HandshakeState { - fn node_pubkey(&self) -> &mlkem1024_x25519::PublicKey { + fn node_pubkey(&self) -> &xwing::PublicKey { &self.materials.node_pubkey.pk } @@ -102,11 +101,31 @@ impl ClientHandshakeMaterials for HandshakeMaterials { /// Client side of the ntor v3 handshake. pub(crate) struct NtorV3Client; -impl ClientHandshake for NtorV3Client { - type StateType = HandshakeState; +/// State resulting from successful client handshake. +pub struct HsComplete { + keygen: NtorV3KeyGenerator, + extensions: Vec, + remainder: (), +} +impl ClientHandshakeComplete for HsComplete { type KeyGen = NtorV3KeyGenerator; type ServerAuxData = Vec; + type Remainder = (); + fn keygen(&self) -> &Self::KeyGen { + &self.keygen + } + fn extensions(&self) -> &Self::ServerAuxData { + &self.extensions + } + fn remainder(&self) -> &Self::Remainder { + &self.remainder + } +} + +impl ClientHandshake for NtorV3Client { + type StateType = HandshakeState; type HandshakeMaterials = HandshakeMaterials; + type HsOutput = HsComplete; /// Generate a new client onionskin for a relay with a given onion key. /// If any `extensions` are provided, encode them into to the onionskin. @@ -126,10 +145,7 @@ impl ClientHandshake for NtorV3Client { /// /// The state object must match the one that was used to make the /// client onionskin that the server is replying to. - fn client2>( - state: Self::StateType, - msg: T, - ) -> Result<(Vec, Self::KeyGen)> { + fn client2>(state: Self::StateType, msg: T) -> Result { let (message, xofreader) = client_handshake_ntor_v3_part2(&state, msg.as_ref(), NTOR3_CIRC_VERIFICATION)?; let extensions = NtorV3Extension::decode(&message).map_err(|err| Error::CellDecodeErr { @@ -138,7 +154,11 @@ impl ClientHandshake for NtorV3Client { })?; let keygen = NtorV3KeyGenerator::new::(xofreader); - Ok((extensions, keygen)) + Ok(HsComplete { + extensions, + keygen, + remainder: (), + }) } } @@ -163,50 +183,52 @@ pub(crate) fn client_handshake_ntor_v3_no_keygen( materials: HandshakeMaterials, verification: &[u8], ) -> EncodeResult<(HandshakeState, Vec)> { - let my_public = SessionPublicKey::from(&my_sk); - let client_msg = ClientHandshakeMessage::new(my_public.clone(), &materials); - - // -------- - let node_pubkey = materials.node_pubkey(); - // let bx = my_sk.diffie_hellman(&node_pubkey); - let mut rng = rand::thread_rng(); - let (ct, bx) = my_sk.hpke(&mut rng, materials.node_pubkey.pk)?; - // .map_err(|e| Error::Crypto(e.to_string())); - - let (enc_key, mut mac) = kdf_msgkdf(&bx, node_pubkey, &my_public, verification)?; - - // encrypted_msg = ENC(ENC_K1, CM) - // msg_mac = MAC_msgmac(MAC_K1, ID | B | X | encrypted_msg) - let encrypted_msg = encrypt(&enc_key, client_msg); - let msg_mac: DigestVal = { - use digest::Digest; - mac.write(&encrypted_msg)?; - mac.take().finalize().into() - }; - - let mut message = Vec::new(); - message.write(&node_pubkey.id)?; - message.write(&node_pubkey.pk.as_bytes())?; - message.write(&my_public.as_bytes())?; - message.write(&encrypted_msg)?; - message.write(&msg_mac)?; - // -------- - - let mut buf = BytesMut::with_capacity(MAX_HANDSHAKE_LENGTH); - let mut hmac_key = materials.node_pubkey.pk.as_bytes().to_vec(); - hmac_key.append(&mut materials.node_pubkey.id.as_bytes().to_vec()); - client_msg.marshall(&mut buf, &hmac_key[..]); - let message = buf.to_vec(); - - let state = HandshakeState { - materials, - my_sk, - shared_secret: bx, - msg_mac, - epoch_hr: client_msg.get_epoch_hr(), - }; - - Ok((state, message)) + todo!("client handshake part 1"); + + // let my_public = SessionPublicKey::from(&my_sk); + // let client_msg = ClientHandshakeMessage::new(my_public.clone(), &materials); + + // // -------- + // let node_pubkey = materials.node_pubkey(); + // // let bx = my_sk.diffie_hellman(&node_pubkey); + // let mut rng = rand::thread_rng(); + // let (ct, bx) = my_sk.hpke(&mut rng, materials.node_pubkey.pk)?; + // // .map_err(|e| Error::Crypto(e.to_string())); + + // let (enc_key, mut mac) = kdf_msgkdf(&bx, node_pubkey, &my_public, verification)?; + + // // encrypted_msg = ENC(ENC_K1, CM) + // // msg_mac = MAC_msgmac(MAC_K1, ID | B | X | encrypted_msg) + // let encrypted_msg = encrypt(&enc_key, client_msg); + // let msg_mac: DigestVal = { + // use digest::Digest; + // mac.write(&encrypted_msg)?; + // mac.take().finalize().into() + // }; + + // let mut message = Vec::new(); + // message.write(&node_pubkey.id)?; + // message.write(&node_pubkey.pk.as_bytes())?; + // message.write(&my_public.as_bytes())?; + // message.write(&encrypted_msg)?; + // message.write(&msg_mac)?; + // // -------- + + // let mut buf = BytesMut::with_capacity(MAX_HANDSHAKE_LENGTH); + // let mut hmac_key = materials.node_pubkey.pk.as_bytes().to_vec(); + // hmac_key.append(&mut materials.node_pubkey.id.as_bytes().to_vec()); + // client_msg.marshall(&mut buf, &hmac_key[..]); + // let message = buf.to_vec(); + + // let state = HandshakeState { + // materials, + // my_sk, + // shared_secret: bx, + // msg_mac, + // epoch_hr: client_msg.get_epoch_hr(), + // }; + + // Ok((state, message)) } /// Finalize the handshake on the client side. @@ -221,72 +243,74 @@ pub(crate) fn client_handshake_ntor_v3_part2( relay_handshake: &[u8], verification: &[u8], ) -> Result<(Vec, NtorV3XofReader)> { - let mut reader = Reader::from_slice(relay_handshake); - let y_pk: SessionPublicKey = reader - .extract() - .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; - let auth: DigestVal = reader - .extract() - .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; - let encrypted_msg = reader.into_rest(); - let my_public = SessionPublicKey::from(&state.my_sk); - - // TODO: Some of this code is duplicated from the server handshake code! It - // would be better to factor it out. - let yx = state.my_sk.diffie_hellman(&y_pk); - let secret_input = { - let mut si = SecretBuf::new(); - si.write(&yx) - .and_then(|_| si.write(&state.shared_secret.as_bytes())) - .and_then(|_| si.write(&state.node_id())) - .and_then(|_| si.write(&state.node_pubkey().as_bytes())) - .and_then(|_| si.write(&my_public.as_bytes())) - .and_then(|_| si.write(&y_pk.as_bytes())) - .and_then(|_| si.write(PROTOID)) - .and_then(|_| si.write(&Encap(verification))) - .map_err(into_internal!("error encoding ntor3 secret_input"))?; - si - }; - let ntor_key_seed = h_key_seed(&secret_input); - let verify = h_verify(&secret_input); - - let computed_auth: DigestVal = { - use digest::Digest; - let mut auth = DigestWriter(Sha3_256::default()); - auth.write(&T_AUTH) - .and_then(|_| auth.write(&verify)) - .and_then(|_| auth.write(&state.node_id())) - .and_then(|_| auth.write(&state.node_pubkey().as_bytes())) - .and_then(|_| auth.write(&y_pk.as_bytes())) - .and_then(|_| auth.write(&my_public.as_bytes())) - .and_then(|_| auth.write(&state.msg_mac)) - .and_then(|_| auth.write(&Encap(encrypted_msg))) - .and_then(|_| auth.write(PROTOID)) - .and_then(|_| auth.write(&b"Server"[..])) - .map_err(into_internal!("error encoding ntor3 authentication input"))?; - auth.take().finalize().into() - }; - - let okay = computed_auth.ct_eq(&auth) - & ct::bool_to_choice(yx.was_contributory()) - & ct::bool_to_choice(state.shared_secret.was_contributory()); - - let (enc_key, keystream) = { - use digest::{ExtendableOutput, XofReader}; - let mut xof = DigestWriter(Shake256::default()); - xof.write(&T_FINAL) - .and_then(|_| xof.write(&ntor_key_seed)) - .map_err(into_internal!("error encoding ntor3 xof input"))?; - let mut r = xof.take().finalize_xof(); - let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); - r.read(&mut enc_key[..]); - (enc_key, r) - }; - let server_reply = decrypt(&enc_key, encrypted_msg); - - if okay.into() { - Ok((server_reply, NtorV3XofReader::new(keystream))) - } else { - Err(Error::BadCircHandshakeAuth) - } + todo!("client handshake part 2"); + + // let mut reader = Reader::from_slice(relay_handshake); + // let y_pk: SessionPublicKey = reader + // .extract() + // .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; + // let auth: DigestVal = reader + // .extract() + // .map_err(|e| Error::from_bytes_err(e, "v3 ntor handshake"))?; + // let encrypted_msg = reader.into_rest(); + // let my_public = SessionPublicKey::from(&state.my_sk); + + // // TODO: Some of this code is duplicated from the server handshake code! It + // // would be better to factor it out. + // let yx = state.my_sk.diffie_hellman(&y_pk); + // let secret_input = { + // let mut si = SecretBuf::new(); + // si.write(&yx) + // .and_then(|_| si.write(&state.shared_secret.as_bytes())) + // .and_then(|_| si.write(&state.node_id())) + // .and_then(|_| si.write(&state.node_pubkey().as_bytes())) + // .and_then(|_| si.write(&my_public.as_bytes())) + // .and_then(|_| si.write(&y_pk.as_bytes())) + // .and_then(|_| si.write(PROTOID)) + // .and_then(|_| si.write(&Encap(verification))) + // .map_err(into_internal!("error encoding ntor3 secret_input"))?; + // si + // }; + // let ntor_key_seed = h_key_seed(&secret_input); + // let verify = h_verify(&secret_input); + + // let computed_auth: DigestVal = { + // use digest::Digest; + // let mut auth = DigestWriter(Sha3_256::default()); + // auth.write(&T_AUTH) + // .and_then(|_| auth.write(&verify)) + // .and_then(|_| auth.write(&state.node_id())) + // .and_then(|_| auth.write(&state.node_pubkey().as_bytes())) + // .and_then(|_| auth.write(&y_pk.as_bytes())) + // .and_then(|_| auth.write(&my_public.as_bytes())) + // .and_then(|_| auth.write(&state.msg_mac)) + // .and_then(|_| auth.write(&Encap(encrypted_msg))) + // .and_then(|_| auth.write(PROTOID)) + // .and_then(|_| auth.write(&b"Server"[..])) + // .map_err(into_internal!("error encoding ntor3 authentication input"))?; + // auth.take().finalize().into() + // }; + + // let okay = computed_auth.ct_eq(&auth) + // & ct::bool_to_choice(yx.was_contributory()) + // & ct::bool_to_choice(state.shared_secret.was_contributory()); + + // let (enc_key, keystream) = { + // use digest::{ExtendableOutput, XofReader}; + // let mut xof = DigestWriter(Shake256::default()); + // xof.write(&T_FINAL) + // .and_then(|_| xof.write(&ntor_key_seed)) + // .map_err(into_internal!("error encoding ntor3 xof input"))?; + // let mut r = xof.take().finalize_xof(); + // let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); + // r.read(&mut enc_key[..]); + // (enc_key, r) + // }; + // let server_reply = decrypt(&enc_key, encrypted_msg); + + // if okay.into() { + // Ok((server_reply, NtorV3XofReader::new(keystream))) + // } else { + // Err(Error::BadCircHandshakeAuth) + // } } diff --git a/crates/o5/src/handshake/keys.rs b/crates/o5/src/handshake/keys.rs index 5d71f9e..1ed5fcc 100644 --- a/crates/o5/src/handshake/keys.rs +++ b/crates/o5/src/handshake/keys.rs @@ -1,9 +1,9 @@ use super::*; use crate::{ common::{ - // kdf::{Kdf, Ntor1Kdf}, - mlkem1024_x25519::{self, Ciphertext, PublicKey, SharedSecret, StaticSecret}, ntor_arti::{KeyGenerator, SessionID, SessionIdentifier}, + // kdf::{Kdf, Ntor1Kdf}, + xwing::{self, Ciphertext, PublicKey, SharedSecret, StaticSecret}, }, constants::*, framing::{O5Codec, KEY_MATERIAL_LENGTH}, @@ -40,14 +40,11 @@ impl From<&IdentitySecretKey> for IdentityPublicKey { } impl IdentityPublicKey { - const CERT_LENGTH: usize = mlkem1024_x25519::PUBKEY_LEN; + const CERT_LENGTH: usize = xwing::PUBKEY_LEN; const CERT_SUFFIX: &'static str = "=="; /// Construct a new IdentityPublicKey from its components. #[allow(unused)] - pub(crate) fn new( - pk: [u8; mlkem1024_x25519::PUBKEY_LEN], - id: [u8; NODE_ID_LENGTH], - ) -> Result { + pub(crate) fn new(pk: [u8; xwing::PUBKEY_LEN], id: [u8; NODE_ID_LENGTH]) -> Result { Ok(Self { pk: pk.try_into()?, id: id.into(), @@ -110,7 +107,7 @@ impl IdentitySecretKey { pub fn try_from_bytes(bytes: impl AsRef<[u8]>) -> Result { let buf = bytes.as_ref(); - if buf.len() < mlkem1024_x25519::PRIVKEY_LEN + NODE_ID_LENGTH { + if buf.len() < xwing::PRIVKEY_LEN + NODE_ID_LENGTH { return Err(Error::new("bad station identity cert provided")); } @@ -159,6 +156,18 @@ impl TryFrom<&[u8]> for IdentitySecretKey { } } +impl Into for &IdentityPublicKey { + fn into(self) -> xwing::PublicKey { + self.pk.clone() + } +} + +impl Into for &IdentitySecretKey { + fn into(self) -> xwing::PublicKey { + self.pk.pk.clone() + } +} + pub trait NtorV3KeyGen: KeyGenerator + SessionIdentifier + Into {} /// Opaque wrapper type for NtorV3's hash reader. diff --git a/crates/o5/src/handshake/server.rs b/crates/o5/src/handshake/server.rs index e5a6cf3..f75da4d 100644 --- a/crates/o5/src/handshake/server.rs +++ b/crates/o5/src/handshake/server.rs @@ -109,117 +109,119 @@ pub(crate) fn server_handshake_ntor_v3_no_keygen RelayHandshakeResult<(Vec, NtorV3XofReader)> { - // Decode the message. - let mut r = Reader::from_slice(message); - let id: Ed25519Identity = r.extract()?; - let requested_pk: IdentityPublicKey = r.extract()?; - let client_pk: SessionPublicKey = r.extract()?; - let client_msg = if let Some(msg_len) = r.remaining().checked_sub(MAC_LEN) { - r.take(msg_len)? - } else { - let deficit = (MAC_LEN - r.remaining()) - .try_into() - .expect("miscalculated!"); - return Err(Error::incomplete_error(deficit).into()); - }; - - let msg_mac: MessageMac = r.extract()?; - r.should_be_exhausted()?; - - // See if we recognize the provided (id,requested_pk) pair. - let keypair = match keys.matches(id, requested_pk.pk).into() { - Some(k) => keys, - None => return Err(RelayHandshakeError::MissingKey), - }; - - let xb = keypair - .hpke(rng, &client_pk) - .map_err(|e| Error::Crypto(e.into()))?; - let (enc_key, mut mac) = kdf_msgkdf(&xb, &keypair.pk, &client_pk, verification) - .map_err(into_internal!("Can't apply ntor3 kdf."))?; - // Verify the message we received. - let computed_mac: DigestVal = { - mac.write(client_msg) - .map_err(into_internal!("Can't compute MAC input."))?; - mac.take().finalize().into() - }; - let y_pk = SessionPublicKey::from(secret_key_y); - let xy = secret_key_y.hpke(rng, &client_pk)?; - - let mut okay = computed_mac.ct_eq(&msg_mac) - & ct::bool_to_choice(xy.was_contributory()) - & ct::bool_to_choice(xb.was_contributory()); - - let plaintext_msg = decrypt(&enc_key, client_msg); - - // Handle the message and decide how to reply. - let reply = reply_fn.reply(&plaintext_msg); - - // It's not exactly constant time to use is_some() and - // unwrap_or_else() here, but that should be somewhat - // hidden by the rest of the computation. - okay &= ct::bool_to_choice(reply.is_some()); - let reply = reply.unwrap_or_default(); - - // If we reach this point, we are actually replying, or pretending - // that we're going to reply. - - let secret_input = { - let mut si = SecretBuf::new(); - si.write(&xy.as_bytes()) - .and_then(|_| si.write(&xb.as_bytes())) - .and_then(|_| si.write(&keypair.pk.id)) - .and_then(|_| si.write(&keypair.pk.pk.as_bytes())) - .and_then(|_| si.write(&client_pk.as_bytes())) - .and_then(|_| si.write(&y_pk.as_bytes())) - .and_then(|_| si.write(PROTOID)) - .and_then(|_| si.write(&Encap(verification))) - .map_err(into_internal!("can't derive ntor3 secret_input"))?; - si - }; - let ntor_key_seed = h_key_seed(&secret_input); - let verify = h_verify(&secret_input); - - let (enc_key, keystream) = { - let mut xof = DigestWriter(Shake256::default()); - xof.write(&T_FINAL) - .and_then(|_| xof.write(&ntor_key_seed)) - .map_err(into_internal!("can't generate ntor3 xof."))?; - let mut r = xof.take().finalize_xof(); - let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); - r.read(&mut enc_key[..]); - (enc_key, r) - }; - let encrypted_reply = encrypt(&enc_key, &reply); - let auth: DigestVal = { - let mut auth = DigestWriter(Sha3_256::default()); - auth.write(&T_AUTH) - .and_then(|_| auth.write(&verify)) - .and_then(|_| auth.write(&keypair.pk.id)) - .and_then(|_| auth.write(&keypair.pk.pk.as_bytes())) - .and_then(|_| auth.write(&y_pk.as_bytes())) - .and_then(|_| auth.write(&client_pk.as_bytes())) - .and_then(|_| auth.write(&msg_mac)) - .and_then(|_| auth.write(&Encap(&encrypted_reply))) - .and_then(|_| auth.write(PROTOID)) - .and_then(|_| auth.write(&b"Server"[..])) - .map_err(into_internal!("can't derive ntor3 authentication"))?; - auth.take().finalize().into() - }; - - let reply = { - let mut reply = Vec::new(); - reply - .write(&y_pk.as_bytes()) - .and_then(|_| reply.write(&auth)) - .and_then(|_| reply.write(&encrypted_reply)) - .map_err(into_internal!("can't encode ntor3 reply."))?; - reply - }; - - if okay.into() { - Ok((reply, NtorV3XofReader::new(keystream))) - } else { - Err(RelayHandshakeError::BadClientHandshake) - } + todo!("server handshake"); + + // // Decode the message. + // let mut r = Reader::from_slice(message); + // let id: Ed25519Identity = r.extract()?; + // let requested_pk: IdentityPublicKey = r.extract()?; + // let client_pk: SessionPublicKey = r.extract()?; + // let client_msg = if let Some(msg_len) = r.remaining().checked_sub(MAC_LEN) { + // r.take(msg_len)? + // } else { + // let deficit = (MAC_LEN - r.remaining()) + // .try_into() + // .expect("miscalculated!"); + // return Err(Error::incomplete_error(deficit).into()); + // }; + + // let msg_mac: MessageMac = r.extract()?; + // r.should_be_exhausted()?; + + // // See if we recognize the provided (id,requested_pk) pair. + // let keypair = match keys.matches(id, requested_pk.pk).into() { + // Some(k) => keys, + // None => return Err(RelayHandshakeError::MissingKey), + // }; + + // let xb = keypair + // .hpke(rng, &client_pk) + // .map_err(|e| Error::Crypto(e.into()))?; + // let (enc_key, mut mac) = kdf_msgkdf(&xb, &keypair.pk, &client_pk, verification) + // .map_err(into_internal!("Can't apply ntor3 kdf."))?; + // // Verify the message we received. + // let computed_mac: DigestVal = { + // mac.write(client_msg) + // .map_err(into_internal!("Can't compute MAC input."))?; + // mac.take().finalize().into() + // }; + // let y_pk = SessionPublicKey::from(secret_key_y); + // let xy = secret_key_y.hpke(rng, &client_pk)?; + + // let mut okay = computed_mac.ct_eq(&msg_mac) + // & ct::bool_to_choice(xy.was_contributory()) + // & ct::bool_to_choice(xb.was_contributory()); + + // let plaintext_msg = decrypt(&enc_key, client_msg); + + // // Handle the message and decide how to reply. + // let reply = reply_fn.reply(&plaintext_msg); + + // // It's not exactly constant time to use is_some() and + // // unwrap_or_else() here, but that should be somewhat + // // hidden by the rest of the computation. + // okay &= ct::bool_to_choice(reply.is_some()); + // let reply = reply.unwrap_or_default(); + + // // If we reach this point, we are actually replying, or pretending + // // that we're going to reply. + + // let secret_input = { + // let mut si = SecretBuf::new(); + // si.write(&xy.as_bytes()) + // .and_then(|_| si.write(&xb.as_bytes())) + // .and_then(|_| si.write(&keypair.pk.id)) + // .and_then(|_| si.write(&keypair.pk.pk.as_bytes())) + // .and_then(|_| si.write(&client_pk.as_bytes())) + // .and_then(|_| si.write(&y_pk.as_bytes())) + // .and_then(|_| si.write(PROTOID)) + // .and_then(|_| si.write(&Encap(verification))) + // .map_err(into_internal!("can't derive ntor3 secret_input"))?; + // si + // }; + // let ntor_key_seed = h_key_seed(&secret_input); + // let verify = h_verify(&secret_input); + + // let (enc_key, keystream) = { + // let mut xof = DigestWriter(Shake256::default()); + // xof.write(&T_FINAL) + // .and_then(|_| xof.write(&ntor_key_seed)) + // .map_err(into_internal!("can't generate ntor3 xof."))?; + // let mut r = xof.take().finalize_xof(); + // let mut enc_key = Zeroizing::new([0_u8; ENC_KEY_LEN]); + // r.read(&mut enc_key[..]); + // (enc_key, r) + // }; + // let encrypted_reply = encrypt(&enc_key, &reply); + // let auth: DigestVal = { + // let mut auth = DigestWriter(Sha3_256::default()); + // auth.write(&T_AUTH) + // .and_then(|_| auth.write(&verify)) + // .and_then(|_| auth.write(&keypair.pk.id)) + // .and_then(|_| auth.write(&keypair.pk.pk.as_bytes())) + // .and_then(|_| auth.write(&y_pk.as_bytes())) + // .and_then(|_| auth.write(&client_pk.as_bytes())) + // .and_then(|_| auth.write(&msg_mac)) + // .and_then(|_| auth.write(&Encap(&encrypted_reply))) + // .and_then(|_| auth.write(PROTOID)) + // .and_then(|_| auth.write(&b"Server"[..])) + // .map_err(into_internal!("can't derive ntor3 authentication"))?; + // auth.take().finalize().into() + // }; + + // let reply = { + // let mut reply = Vec::new(); + // reply + // .write(&y_pk.as_bytes()) + // .and_then(|_| reply.write(&auth)) + // .and_then(|_| reply.write(&encrypted_reply)) + // .map_err(into_internal!("can't encode ntor3 reply."))?; + // reply + // }; + + // if okay.into() { + // Ok((reply, NtorV3XofReader::new(keystream))) + // } else { + // Err(RelayHandshakeError::BadClientHandshake) + // } } diff --git a/crates/o5/src/lib.rs b/crates/o5/src/lib.rs index c584223..9203056 100644 --- a/crates/o5/src/lib.rs +++ b/crates/o5/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] +#![allow(unused)] pub mod client; pub mod common; diff --git a/crates/o5/src/pt.rs b/crates/o5/src/pt.rs index 15e0e33..6a86bd8 100644 --- a/crates/o5/src/pt.rs +++ b/crates/o5/src/pt.rs @@ -1,10 +1,14 @@ -use crate::{constants::*, handshake::IdentityPublicKey, proto::O5Stream, Error, TRANSPORT_NAME}; +use crate::{ + common::xwing, constants::*, handshake::IdentityPublicKey, proto::O5Stream, Error, + TRANSPORT_NAME, +}; use ptrs::{args::Args, FutureResult as F}; use std::{ marker::PhantomData, net::{SocketAddrV4, SocketAddrV6}, pin::Pin, + str::FromStr, time::Duration, }; @@ -148,13 +152,8 @@ where } }; - self.with_node_pubkey(server_materials.pk.to_bytes()) - .with_node_id(server_materials.id.into()); - trace!( - "node_pubkey: {}, node_id: {}", - hex::encode(self.station_pubkey), - hex::encode(self.station_id), - ); + self.with_node(server_materials); + trace!("node details: {:?}", &self.node_details,); Ok(self) } diff --git a/crates/o5/src/server.rs b/crates/o5/src/server.rs index 1058d7d..a97a8ae 100644 --- a/crates/o5/src/server.rs +++ b/crates/o5/src/server.rs @@ -285,10 +285,7 @@ impl Server { pub fn client_params(&self) -> ClientBuilder { ClientBuilder { - // these unwraps should be safe as we are sure of the size of the source - station_pubkey: self.identity_keys.pk.pk.as_bytes().try_into().unwrap(), - station_id: self.identity_keys.pk.id.as_bytes().try_into().unwrap(), - + node_details: self.identity_keys.pk.clone(), statefile_path: None, handshake_timeout: MaybeTimeout::Default_, } diff --git a/crates/o5/src/sessions.rs b/crates/o5/src/sessions.rs index b0af194..0ae9a17 100644 --- a/crates/o5/src/sessions.rs +++ b/crates/o5/src/sessions.rs @@ -2,7 +2,7 @@ //! /// Session state management as a way to organize session establishment and /// steady state transfer. -use crate::common::{drbg, mlkem1024_x25519}; +use crate::common::{drbg, xwing}; use tor_bytes::Readable; @@ -13,11 +13,10 @@ mod server; pub(crate) use server::ServerSession; /// Ephermeral single use session secret key type -pub type SessionSecretKey = mlkem1024_x25519::StaticSecret; +pub type SessionSecretKey = xwing::StaticSecret; /// Public key type associated with SessionSecretKey. -pub type SessionPublicKey = mlkem1024_x25519::PublicKey; - +pub type SessionPublicKey = xwing::PublicKey; impl Readable for SessionPublicKey { fn take_from(_b: &mut tor_bytes::Reader<'_>) -> tor_bytes::Result { @@ -25,7 +24,6 @@ impl Readable for SessionPublicKey { } } - /// Initial state for a Session, created with any params. pub(crate) struct Initialized; diff --git a/crates/o5/src/test_utils/mod.rs b/crates/o5/src/test_utils/mod.rs index f88fb88..9b0c220 100644 --- a/crates/o5/src/test_utils/mod.rs +++ b/crates/o5/src/test_utils/mod.rs @@ -2,6 +2,7 @@ #![allow(dead_code)] mod fake_prng; +pub(crate) mod test_keys; pub mod tests; pub(crate) use fake_prng::*; diff --git a/crates/o5/src/test_utils/test_keys.rs b/crates/o5/src/test_utils/test_keys.rs new file mode 100644 index 0000000..136df8b --- /dev/null +++ b/crates/o5/src/test_utils/test_keys.rs @@ -0,0 +1,62 @@ +//! # Keys used for testing +//! +//! The hex representations of X-Wing keys are rather large. This module exists to house +//! those representations so they don't muddy up the whole crate. +//! +//! ## Naming Conventions: +//! +//! id: Node ID +//! b: Node identity secret key +//! B: Node identity public key +//! +//! x: Client session secret Key +//! X: Client session public Key +//! +//! y: Server session secret Key +//! Y: Server session public Key +//! +//! within the X-Wing Keys X indicates an X25519 element, and M indicates an ML-KEM element +//! +//! xX: client session secret key x25519 element +//! xM: client session secret key ML-KEM element +//! +//! +//! The shared secrets used in a handshake are +//! +//! xB: client session secret key KEM with node identity public key +//! Xb: server identity secret key KEM with client session public key +//! +//! xB == Xb == xX*BX + xM*BM = XX*bX + XM*bM +//! +//! Xy: server session secret key KEM with client session public key +//! xY: client session secret key KEM with server session public key +//! +//! xY == Xy == xX*YX + xM*YM = XX*yX + XM*yM + +#[allow(non_snake_case)] +pub struct HexKeys { + pub id: &'static str, + pub b: &'static str, + pub x: &'static str, + pub y: &'static str, + + pub xB: &'static str, + pub xY: &'static str, + + // Ciphertext of x encapsulated using B encoded using Elligator2 and Kemeleon + pub xB_C_EK: &'static str, + + // Ciphertext of y encapsulated using X encoded using elligator2 and Kemeleon. + pub Xy_C_EK: &'static str, +} + +pub const KEYS: [HexKeys; 1] = [HexKeys { + id: "aaaaaaaaaaaaaaaaaaaaaaaa9fad2af287ef942632833d21f946c6260c33fae6", + b: "4051daa5921cfa2a1c27b08451324919538e79e788a81b38cbed097a5dff454a", + x: "b825a3719147bcbe5fb1d0b0fcb9c09e51948048e2e3283d2ab7b45b5ef38b49", + y: "4865a5b7689dafd978f529291c7171bc159be076b92186405d13220b80e2a053", + xB: "", + xY: "", + xB_C_EK: "", + Xy_C_EK: "", +}]; From 7a7fe2b2a52f72b81f7d53e9a33497a2c45566eb Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Mon, 14 Oct 2024 22:26:37 -0600 Subject: [PATCH 3/7] trait work --- crates/o5/src/common/ntor_arti.rs | 6 ++-- crates/o5/src/handshake.rs | 25 +++++++++++------ crates/o5/src/handshake/client.rs | 32 +++++++++++---------- crates/o5/src/handshake/keys.rs | 7 +++-- crates/o5/src/sessions/client.rs | 46 +++++++++++++++++++++---------- 5 files changed, 72 insertions(+), 44 deletions(-) diff --git a/crates/o5/src/common/ntor_arti.rs b/crates/o5/src/common/ntor_arti.rs index 35b6d01..fc8a7ec 100644 --- a/crates/o5/src/common/ntor_arti.rs +++ b/crates/o5/src/common/ntor_arti.rs @@ -85,16 +85,16 @@ pub trait ClientHandshake { /// /// The state object must match the one that was used to make the /// client onionskin that the server is replying to. - fn client2>(state: Self::StateType, msg: T) -> Result<(Self::HsOutput)>; + fn client2>(state: &mut Self::StateType, msg: T) -> Result<(Self::HsOutput)>; } pub trait ClientHandshakeComplete { type KeyGen; type ServerAuxData; type Remainder; - fn keygen(&self) -> &Self::KeyGen; + fn keygen(&self) -> Self::KeyGen; fn extensions(&self) -> &Self::ServerAuxData; - fn remainder(&self) -> &Self::Remainder; + fn remainder(&self) -> Self::Remainder; } /// Trait for an object that handles incoming auxiliary data and diff --git a/crates/o5/src/handshake.rs b/crates/o5/src/handshake.rs index 8dda1c1..2b45ccf 100644 --- a/crates/o5/src/handshake.rs +++ b/crates/o5/src/handshake.rs @@ -5,6 +5,7 @@ //! encrypt data (without forward secrecy) after it sends the first //! message. +use crate::common::ntor_arti::ClientHandshakeComplete; use crate::sessions::SessionPublicKey; use cipher::{KeyIvInit as _, StreamCipher as _}; @@ -16,7 +17,7 @@ use zeroize::Zeroizing; mod keys; use keys::NtorV3XofReader; -pub(crate) use keys::{Authcode, AUTHCODE_LENGTH}; +pub(crate) use keys::{Authcode, NtorV3KeyGenerator, AUTHCODE_LENGTH}; pub use keys::{IdentityPublicKey, IdentitySecretKey, NtorV3KeyGen}; /// Super trait to be used where we require a distinction between client and server roles. @@ -39,7 +40,9 @@ impl Role for ServerRole { } mod client; -pub(crate) use client::{HandshakeMaterials as CHSMaterials, NtorV3Client}; +pub(crate) use client::{ + HandshakeMaterials as CHSMaterials, HsComplete as ClientHsComplete, NtorV3Client, +}; mod server; pub(crate) use server::HandshakeMaterials as SHSMaterials; @@ -268,7 +271,9 @@ mod test { #![allow(clippy::useless_vec)] #![allow(clippy::needless_pass_by_value)] //! - use crate::common::ntor_arti::{ClientHandshake, KeyGenerator, ServerHandshake}; + use crate::common::ntor_arti::{ + ClientHandshake, ClientHandshakeComplete, KeyGenerator, ServerHandshake, + }; use crate::common::xwing::{PublicKey, StaticSecret}; use crate::constants::{NODE_ID_LENGTH, SEED_LENGTH}; use crate::Server; @@ -332,7 +337,7 @@ mod test { let relay_private = IdentitySecretKey::random_from_rng(&mut testing_rng()); let materials = CHSMaterials::new(&relay_private.pk, "fake_session_id-1".into()); - let (c_state, c_handshake) = NtorV3Client::client1(materials).unwrap(); + let (mut c_state, c_handshake) = NtorV3Client::client1(materials).unwrap(); let mut rep = |_: &[NtorV3Extension]| Some(vec![]); @@ -345,8 +350,10 @@ mod test { .server(&mut rep, &shs_materials, &c_handshake) .unwrap(); - let (extensions, keygen) = NtorV3Client::client2(c_state, s_handshake).unwrap(); + let hs_complete = NtorV3Client::client2(&mut c_state, s_handshake).unwrap(); + let extensions = hs_complete.extensions(); + let mut keygen = hs_complete.keygen(); assert!(extensions.is_empty()); let c_keys = keygen.expand(1000).unwrap(); let s_keys = s_keygen.expand(100).unwrap(); @@ -363,7 +370,7 @@ mod test { let materials = CHSMaterials::new(&relay_private.pk, "client_session_1".into()) .with_aux_data([NtorV3Extension::RequestCongestionControl]); - let (c_state, c_handshake) = NtorV3Client::client1(materials).unwrap(); + let (mut c_state, c_handshake) = NtorV3Client::client1(materials).unwrap(); let mut rep = |msg: &[NtorV3Extension]| -> Option> { assert_eq!(msg, client_exts); @@ -379,9 +386,11 @@ mod test { .server(&mut rep, &shs_materials, &c_handshake) .unwrap(); - let (extensions, keygen) = NtorV3Client::client2(c_state, s_handshake).unwrap(); + let hs_complete = NtorV3Client::client2(&mut c_state, s_handshake).unwrap(); - assert_eq!(extensions, reply_exts); + let extensions = hs_complete.extensions(); + let mut keygen = hs_complete.keygen(); + assert_eq!(extensions, &reply_exts); let c_keys = keygen.expand(1000).unwrap(); let s_keys = s_keygen.expand(100).unwrap(); assert_eq!(s_keys[..], c_keys[..100]); diff --git a/crates/o5/src/handshake/client.rs b/crates/o5/src/handshake/client.rs index 85b3d06..991c294 100644 --- a/crates/o5/src/handshake/client.rs +++ b/crates/o5/src/handshake/client.rs @@ -1,7 +1,9 @@ use crate::{ common::{ ct, - ntor_arti::{ClientHandshake, ClientHandshakeComplete, ClientHandshakeMaterials}, + ntor_arti::{ + ClientHandshake, ClientHandshakeComplete, ClientHandshakeMaterials, KeyGenerator, + }, xwing::SharedSecret, }, constants::*, @@ -21,7 +23,7 @@ use tor_bytes::{EncodeResult, Reader, SecretBuf, Writer}; use tor_cell::relaycell::extend::NtorV3Extension; use tor_error::into_internal; use tor_llcrypto::{ - d::{Sha3_256, Shake256}, + d::{Sha3_256, Shake256, Shake256Reader}, pk::ed25519::Ed25519Identity, }; use zeroize::Zeroizing; @@ -38,7 +40,7 @@ pub(crate) struct HandshakeState { my_sk: SessionSecretKey, /// handshake materials - materials: HandshakeMaterials, + pub(crate) materials: HandshakeMaterials, /// the computed hour at which the initial portion of the handshake was sent. epoch_hr: String, @@ -103,22 +105,23 @@ pub(crate) struct NtorV3Client; /// State resulting from successful client handshake. pub struct HsComplete { - keygen: NtorV3KeyGenerator, + xof_reader: NtorV3XofReader, extensions: Vec, - remainder: (), + remainder: BytesMut, } + impl ClientHandshakeComplete for HsComplete { type KeyGen = NtorV3KeyGenerator; type ServerAuxData = Vec; - type Remainder = (); - fn keygen(&self) -> &Self::KeyGen { - &self.keygen + type Remainder = BytesMut; + fn keygen(&self) -> Self::KeyGen { + NtorV3KeyGenerator::new::(self.xof_reader.clone()) } fn extensions(&self) -> &Self::ServerAuxData { &self.extensions } - fn remainder(&self) -> &Self::Remainder { - &self.remainder + fn remainder(&self) -> Self::Remainder { + self.remainder.clone() } } @@ -145,19 +148,18 @@ impl ClientHandshake for NtorV3Client { /// /// The state object must match the one that was used to make the /// client onionskin that the server is replying to. - fn client2>(state: Self::StateType, msg: T) -> Result { - let (message, xofreader) = + fn client2>(state: &mut Self::StateType, msg: T) -> Result { + let (message, xof_reader) = client_handshake_ntor_v3_part2(&state, msg.as_ref(), NTOR3_CIRC_VERIFICATION)?; let extensions = NtorV3Extension::decode(&message).map_err(|err| Error::CellDecodeErr { object: "ntor v3 extensions", err, })?; - let keygen = NtorV3KeyGenerator::new::(xofreader); Ok(HsComplete { + xof_reader, extensions, - keygen, - remainder: (), + remainder: BytesMut::new(), // TODO: ACTUALLY FILL THIS WITH REMAINDER BYTES }) } } diff --git a/crates/o5/src/handshake/keys.rs b/crates/o5/src/handshake/keys.rs index 1ed5fcc..60105a1 100644 --- a/crates/o5/src/handshake/keys.rs +++ b/crates/o5/src/handshake/keys.rs @@ -171,6 +171,7 @@ impl Into for &IdentitySecretKey { pub trait NtorV3KeyGen: KeyGenerator + SessionIdentifier + Into {} /// Opaque wrapper type for NtorV3's hash reader. +#[derive(Clone)] pub(crate) struct NtorV3XofReader(Shake256Reader); impl NtorV3XofReader { @@ -249,9 +250,9 @@ impl KeyGenerator for NtorV3XofReader { } } -impl From for O5Codec { - fn from(keygen: NtorV3KeyGenerator) -> Self { - keygen.codec +impl From for O5Codec { + fn from(value: K) -> Self { + value.into() } } diff --git a/crates/o5/src/sessions/client.rs b/crates/o5/src/sessions/client.rs index aa613cc..51ff584 100644 --- a/crates/o5/src/sessions/client.rs +++ b/crates/o5/src/sessions/client.rs @@ -1,11 +1,17 @@ use crate::{ common::{ discard, drbg, - ntor_arti::{ClientHandshake, RelayHandshakeError, SessionID, SessionIdentifier}, + ntor_arti::{ + ClientHandshake, ClientHandshakeComplete, RelayHandshakeError, SessionID, + SessionIdentifier, + }, }, constants::*, framing, - handshake::{CHSMaterials, IdentityPublicKey, NtorV3Client, NtorV3KeyGen}, + handshake::{ + CHSMaterials, ClientHsComplete, IdentityPublicKey, NtorV3Client, NtorV3KeyGen, + NtorV3KeyGenerator, + }, proto::{O5Stream, ObfuscatedStream}, sessions::{Established, Fault, Initialized, Session}, Error, Result, @@ -13,7 +19,7 @@ use crate::{ use std::io::{Error as IoError, ErrorKind as IoErrorKind}; -use bytes::BytesMut; +use bytes::{BufMut, BytesMut}; use ptrs::{debug, info}; use rand_core::RngCore; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -142,8 +148,9 @@ impl ClientSession { // default deadline let d_def = Instant::now() + CLIENT_HANDSHAKE_TIMEOUT; - let handshake_fut = Self::complete_handshake(&mut stream, materials, deadline); - let (mut remainder, mut keygen) = + let handshake_fut = + Self::complete_handshake::(stream, materials, deadline); + let (mut hs_complete, mut stream) = match tokio::time::timeout_at(deadline.unwrap_or(d_def), handshake_fut).await { Ok(result) => match result { Ok(handshake) => handshake, @@ -166,10 +173,16 @@ impl ClientSession { }; // post-handshake state updates + let mut keygen: NtorV3KeyGenerator = hs_complete.keygen(); session.set_session_id(keygen.session_id()); - let mut codec: framing::O5Codec = keygen.into(); + let mut codec = framing::O5Codec::from(keygen); + + // // TODO: handle server response extensions here + // for ext in hs_complete.extensions() { + // // do something + // } - let res = codec.decode(&mut remainder); + let res = codec.decode(&mut hs_complete.remainder()); if let Ok(Some(framing::Messages::PrngSeed(seed))) = res { // try to parse the remainder of the server hello packet as a // PrngSeed since it should be there. @@ -189,23 +202,26 @@ impl ClientSession { Ok(O5Stream::from_o4(o4)) } - async fn complete_handshake( + async fn complete_handshake( mut stream: T, materials: CHSMaterials, deadline: Option, - ) -> Result<(BytesMut, impl NtorV3KeyGen)> + ) -> Result<( + impl ClientHandshakeComplete, + T, + )> where T: AsyncRead + AsyncWrite + Unpin, { // let session_id = materials.session_id; - let (state, chs_message) = NtorV3Client::client1(materials)?; + let (mut state, chs_message) = NtorV3Client::client1(materials)?; // let mut file = tokio::fs::File::create("message.hex").await?; // file.write_all(&chs_message).await?; stream.write_all(&chs_message).await?; debug!( "{} handshake sent {}B, waiting for sever response", - materials.session_id, + state.materials.session_id, chs_message.len() ); @@ -220,18 +236,18 @@ impl ClientSession { } debug!( "{} read {n}/{}B of server handshake", - materials.session_id, + state.materials.session_id, buf.len() ); - match NtorV3Client::client2(state, &buf[..n]) { - Ok(r) => return Ok(r), + match NtorV3Client::client2(&mut state, &buf[..n]) { + Ok(r) => return Ok((r, stream)), Err(Error::HandshakeErr(RelayHandshakeError::EAgain)) => continue, Err(e) => { // if a deadline was set and has not passed already, discard // from the stream until the deadline, then close. if deadline.is_some_and(|d| d > Instant::now()) { - debug!("{} discarding due to: {e}", materials.session_id); + debug!("{} discarding due to: {e}", state.materials.session_id); discard(&mut stream, deadline.unwrap() - Instant::now()).await?; } stream.shutdown().await?; From 8266529ab56d3ede05b6c4f87d88973213893563 Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:11:03 -0600 Subject: [PATCH 4/7] 05 compiling and tests running. Many test failures and warnings --- crates/lyrebird/src/fwd/main.rs | 14 +++++++------- crates/lyrebird/src/main.rs | 6 +++--- crates/o5/src/handshake/keys.rs | 2 +- crates/o5/src/pt.rs | 6 +++++- crates/obfs4/src/pt.rs | 6 +++++- crates/obfs4/src/server.rs | 2 +- crates/ptrs/src/lib.rs | 9 ++++++++- crates/ptrs/src/passthrough.rs | 33 ++++++++++++++++++++------------- 8 files changed, 50 insertions(+), 28 deletions(-) diff --git a/crates/lyrebird/src/fwd/main.rs b/crates/lyrebird/src/fwd/main.rs index ea78049..e20e8bd 100644 --- a/crates/lyrebird/src/fwd/main.rs +++ b/crates/lyrebird/src/fwd/main.rs @@ -442,15 +442,15 @@ where let mut listeners = Vec::new(); let mut builder = Obfs4PT::server_builder(); - let server = if params.is_some() { - builder.options(¶ms.unwrap())?.build() - } else { - builder.build() - }; + if params.is_some() { + builder.options(¶ms.unwrap())?; + } + + let client_options = builder.get_client_params(); + let server = builder.build(); info!( - "({obfs4_name}) client params: \"{}\"", - builder.get_client_params() + "({obfs4_name}) client params: \"{}\"", client_options ); let listener = tokio::net::TcpListener::bind(listen_addrs).await?; diff --git a/crates/lyrebird/src/main.rs b/crates/lyrebird/src/main.rs index 62480c4..631621f 100644 --- a/crates/lyrebird/src/main.rs +++ b/crates/lyrebird/src/main.rs @@ -527,10 +527,10 @@ async fn server_setup( } let mut builder = Obfs4PT::server_builder(); - let server = builder + builder .statefile_location(statedir)? - .options(&bind_addr.options)? - .build(); + .options(&bind_addr.options)?; + let server = builder.build(); let listener = tokio::net::TcpListener::bind(bind_addr.addr).await?; listeners.push(server_listen_loop::( diff --git a/crates/o5/src/handshake/keys.rs b/crates/o5/src/handshake/keys.rs index 60105a1..626dc81 100644 --- a/crates/o5/src/handshake/keys.rs +++ b/crates/o5/src/handshake/keys.rs @@ -187,7 +187,7 @@ impl digest::XofReader for NtorV3XofReader { } /// A key generator returned from an ntor v3 handshake. -pub(crate) struct NtorV3KeyGenerator { +pub struct NtorV3KeyGenerator { /// The underlying `digest::XofReader`. reader: NtorV3XofReader, session_id: SessionID, diff --git a/crates/o5/src/pt.rs b/crates/o5/src/pt.rs index 6a86bd8..cbfa976 100644 --- a/crates/o5/src/pt.rs +++ b/crates/o5/src/pt.rs @@ -57,7 +57,7 @@ where type Error = Error; type Transport = Transport; - fn build(&self) -> Self::ServerPT { + fn build(self) -> Self::ServerPT { crate::ServerBuilder::build(self) } @@ -224,6 +224,10 @@ where fn method_name() -> String { TRANSPORT_NAME.into() } + + fn get_client_params(&self) -> String { + self.client_params().as_opts() + } } #[cfg(test)] diff --git a/crates/obfs4/src/pt.rs b/crates/obfs4/src/pt.rs index 7645213..1faf99d 100644 --- a/crates/obfs4/src/pt.rs +++ b/crates/obfs4/src/pt.rs @@ -59,7 +59,7 @@ where type Error = Error; type Transport = Transport; - fn build(&self) -> Self::ServerPT { + fn build(self) -> Self::ServerPT { crate::ServerBuilder::build(self) } @@ -246,6 +246,10 @@ where fn method_name() -> String { OBFS4_NAME.into() } + + fn get_client_params(&self) -> String { + self.client_params().as_opts() + } } #[cfg(test)] diff --git a/crates/obfs4/src/server.rs b/crates/obfs4/src/server.rs index d7dfd42..0df76fa 100644 --- a/crates/obfs4/src/server.rs +++ b/crates/obfs4/src/server.rs @@ -103,7 +103,7 @@ impl ServerBuilder { params.encode_smethod_args() } - pub fn build(&self) -> Server { + pub fn build(self) -> Server { Server(Arc::new(ServerInner { identity_keys: self.identity_keys.clone(), iat_mode: self.iat_mode, diff --git a/crates/ptrs/src/lib.rs b/crates/ptrs/src/lib.rs index e80cee8..d26eeef 100644 --- a/crates/ptrs/src/lib.rs +++ b/crates/ptrs/src/lib.rs @@ -127,6 +127,13 @@ where /// Returns a string identifier for this transport fn method_name() -> String; + + /// Returns a string or parameters that can be used by a ['ClientBuilder'] + /// in the `options(...)` function to properly establish a connection with + /// this server based on the configuration of the server when this method + /// is called. + fn get_client_params(&self) -> String; + } /// Server Transport builder interface @@ -145,7 +152,7 @@ where /// /// **Errors** /// If a required field has not been initialized. - fn build(&self) -> Self::ServerPT; + fn build(self) -> Self::ServerPT; /// Pluggable transport attempts to parse and validate options from a string, /// typically using ['parse_smethod_args']. diff --git a/crates/ptrs/src/passthrough.rs b/crates/ptrs/src/passthrough.rs index 003b0e1..8fc7672 100644 --- a/crates/ptrs/src/passthrough.rs +++ b/crates/ptrs/src/passthrough.rs @@ -39,7 +39,7 @@ where String::from("passthrough") } - fn build(&self) -> Self::ServerPT { + fn build(self) -> Self::ServerPT { Passthrough {} } @@ -155,6 +155,10 @@ where fn method_name() -> String { String::from("passthrough") } + + fn get_client_params(&self) -> String { + String::new() + } } impl Passthrough { @@ -520,11 +524,12 @@ mod design_tests { <

>::ServerBuilder as ServerBuilder>::ServerPT: ServerTransport, <

>::ServerBuilder as ServerBuilder>::Error: std::error::Error + 'static, { - Ok(P::server_builder() + let mut builder = P::server_builder(); + builder .statefile_location("./")? - .timeout(Some(Duration::from_secs(30)))? - .build() - .reveal(t)) + .timeout(Some(Duration::from_secs(30)))?; + let server = builder.build(); + Ok(server.reveal(t)) } #[tokio::test] @@ -599,11 +604,11 @@ mod design_tests { B::Error: std::error::Error + 'static, >::Error: std::error::Error + Send + Sync, { - Ok(pt_builder + pt_builder .statefile_location("./")? - .timeout(Some(Duration::from_secs(30)))? - .build() - .reveal(t)) + .timeout(Some(Duration::from_secs(30)))?; + + Ok(pt_builder.build().reveal(t)) } #[tokio::test] @@ -763,15 +768,17 @@ mod design_tests { let (tcp_sock, _) = listener.accept().await.unwrap(); - let pb: &BuilderS = &>::server_builder(); + let pb1: BuilderS = >::server_builder(); + let pb2: BuilderS = >::server_builder(); + let pb3: BuilderS = >::server_builder(); - let client1 = >::build(pb); + let client1 = >::build(pb1); let conn1 = client1.reveal(tcp_sock).await.unwrap(); - let client2 = >::build(pb); + let client2 = >::build(pb2); let conn2 = client2.reveal(conn1).await.unwrap(); - let client3 = >::build(pb); + let client3 = >::build(pb3); let mut sock = client3.reveal(conn2).await.unwrap(); let (mut r, mut w) = tokio::io::split(&mut sock); From ca8c3570254eeae1f62e01026a98e773377fc4cb Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:32:28 -0600 Subject: [PATCH 5/7] solve most warnings --- crates/o5/src/common/xwing.rs | 2 +- crates/o5/src/framing/handshake.rs | 2 +- crates/o5/src/handshake.rs | 6 +++--- crates/o5/src/handshake/keys.rs | 2 +- crates/o5/src/lib.rs | 2 ++ 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/o5/src/common/xwing.rs b/crates/o5/src/common/xwing.rs index 1a57cfc..02e56e1 100644 --- a/crates/o5/src/common/xwing.rs +++ b/crates/o5/src/common/xwing.rs @@ -40,7 +40,7 @@ struct HybridKey { } #[derive(Clone, PartialEq)] -pub(crate) struct PublicKey { +pub struct PublicKey { x25519: x25519_dalek::PublicKey, mlkem: EncapsulationKey, pub_key: [u8; PUBKEY_LEN], diff --git a/crates/o5/src/framing/handshake.rs b/crates/o5/src/framing/handshake.rs index 1e68468..e725a41 100644 --- a/crates/o5/src/framing/handshake.rs +++ b/crates/o5/src/framing/handshake.rs @@ -108,7 +108,7 @@ pub struct ClientHandshakeMessage<'a> { } impl<'a> ClientHandshakeMessage<'a> { - pub fn new(client_session_pubkey: SessionPublicKey, hs_materials: &'a CHSMaterials) -> Self { + pub(crate) fn new(client_session_pubkey: SessionPublicKey, hs_materials: &'a CHSMaterials) -> Self { Self { hs_materials, client_session_pubkey, diff --git a/crates/o5/src/handshake.rs b/crates/o5/src/handshake.rs index 2b45ccf..1e8a630 100644 --- a/crates/o5/src/handshake.rs +++ b/crates/o5/src/handshake.rs @@ -21,18 +21,18 @@ pub(crate) use keys::{Authcode, NtorV3KeyGenerator, AUTHCODE_LENGTH}; pub use keys::{IdentityPublicKey, IdentitySecretKey, NtorV3KeyGen}; /// Super trait to be used where we require a distinction between client and server roles. -trait Role { +pub trait Role { fn is_client() -> bool; } -struct ClientRole {} +pub struct ClientRole {} impl Role for ClientRole { fn is_client() -> bool { true } } -struct ServerRole {} +pub struct ServerRole {} impl Role for ServerRole { fn is_client() -> bool { false diff --git a/crates/o5/src/handshake/keys.rs b/crates/o5/src/handshake/keys.rs index 626dc81..ae85f06 100644 --- a/crates/o5/src/handshake/keys.rs +++ b/crates/o5/src/handshake/keys.rs @@ -205,7 +205,7 @@ impl KeyGenerator for NtorV3KeyGenerator { impl NtorV3KeyGen for NtorV3KeyGenerator {} impl NtorV3KeyGenerator { - pub(crate) fn new(mut reader: NtorV3XofReader) -> Self { + pub fn new(mut reader: NtorV3XofReader) -> Self { // let okm = Self::kdf(&seed[..], KEY_MATERIAL_LENGTH * 2 + SESSION_ID_LEN) // .expect("bug: failed to derive key material from seed"); diff --git a/crates/o5/src/lib.rs b/crates/o5/src/lib.rs index 9203056..88816ec 100644 --- a/crates/o5/src/lib.rs +++ b/crates/o5/src/lib.rs @@ -1,4 +1,6 @@ #![doc = include_str!("../README.md")] + +// #![warn(missing_docs)] #![allow(unused)] pub mod client; From e91bc0bbc87d54ed5c7b1fd38984f57836bd32e5 Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Sun, 20 Oct 2024 08:55:15 -0600 Subject: [PATCH 6/7] fix trivial issues to focus on impl --- crates/lyrebird/src/fwd/main.rs | 4 +- crates/o5/Cargo.toml | 1 + crates/o5/src/client.rs | 2 +- crates/o5/src/common/xwing.rs | 269 ++++++++++------------------- crates/o5/src/framing/handshake.rs | 5 +- crates/o5/src/handshake.rs | 25 ++- crates/o5/src/handshake/client.rs | 10 +- crates/o5/src/handshake/keys.rs | 58 +++---- crates/o5/src/handshake/server.rs | 6 +- crates/o5/src/lib.rs | 1 - crates/o5/src/pt.rs | 2 +- crates/o5/src/sessions.rs | 4 +- crates/o5/src/sessions/client.rs | 2 +- crates/ptrs/src/lib.rs | 1 - 14 files changed, 142 insertions(+), 248 deletions(-) diff --git a/crates/lyrebird/src/fwd/main.rs b/crates/lyrebird/src/fwd/main.rs index e20e8bd..ea4c659 100644 --- a/crates/lyrebird/src/fwd/main.rs +++ b/crates/lyrebird/src/fwd/main.rs @@ -449,9 +449,7 @@ where let client_options = builder.get_client_params(); let server = builder.build(); - info!( - "({obfs4_name}) client params: \"{}\"", client_options - ); + info!("({obfs4_name}) client params: \"{}\"", client_options); let listener = tokio::net::TcpListener::bind(listen_addrs).await?; listeners.push(server_listen_loop::( diff --git a/crates/o5/Cargo.toml b/crates/o5/Cargo.toml index 60a8622..77117b1 100644 --- a/crates/o5/Cargo.toml +++ b/crates/o5/Cargo.toml @@ -35,6 +35,7 @@ hkdf = "0.12.3" crypto_secretbox = { version="0.1.1", features=["chacha20"]} subtle = "2.5.0" x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom", "reusable_secrets"]} +x-wing = "0.0.1-alpha" ## Utils pin-project = "1.1.3" diff --git a/crates/o5/src/client.rs b/crates/o5/src/client.rs index 0497a0d..04a90db 100644 --- a/crates/o5/src/client.rs +++ b/crates/o5/src/client.rs @@ -64,7 +64,7 @@ impl ClientBuilder { } pub fn with_node_pubkey(&mut self, pubkey: [u8; PUBLIC_KEY_LEN]) -> Result<&mut Self> { - self.node_details.pk = xwing::PublicKey::try_from(&pubkey[..])?; + self.node_details.ek = xwing::EncapsulationKey::try_from(&pubkey[..])?; Ok(self) } diff --git a/crates/o5/src/common/xwing.rs b/crates/o5/src/common/xwing.rs index 02e56e1..7e27de8 100644 --- a/crates/o5/src/common/xwing.rs +++ b/crates/o5/src/common/xwing.rs @@ -1,22 +1,9 @@ -//! Combined Kyber (ML-KEM) 1024 and X25519 Hybrid Public Key Encryption (HPKE) scheme. +//! X-Wing Hybrid Public Key Encapsulation //! -//! > For the client's share, the key_exchange value contains the -//! > concatenation of the client's X25519 ephemeral share (32 bytes) and -//! > the client's Kyber768Draft00 public key (1184 bytes). The resulting -//! > key_exchange value is 1216 bytes in length. -//! > -//! > For the server's share, the key_exchange value contains the -//! > concatenation of the server's X25519 ephemeral share (32 bytes) and -//! > the Kyber768Draft00 ciphertext (1088 bytes) returned from -//! > encapsulation for the client's public key. The resulting -//! > key_exchange value is 1120 bytes in length. -//! > -//! > The shared secret is calculated as the concatenation of the X25519 -//! > shared secret (32 bytes) and the Kyber768Draft00 shared secret (32 -//! > bytes). The resulting shared secret value is 64 bytes in length. +//! todo! use kem::{Decapsulate, Encapsulate}; -use kemeleon::{DecapsulationKey, EncapsulationKey, Encode, OKemCore}; +use kemeleon::{Encode, OKemCore}; use rand::{CryptoRng, RngCore}; use rand_core::CryptoRngCore; use subtle::ConstantTimeEq; @@ -31,99 +18,115 @@ pub(crate) const MLKEM1024_PUBKEY_LEN: usize = 1530; pub(crate) const PUBKEY_LEN: usize = MLKEM1024_PUBKEY_LEN + X25519_PUBKEY_LEN; pub(crate) const PRIVKEY_LEN: usize = 1; -pub struct StaticSecret(HybridKey); +pub struct DecapsulationKey { + decap: x_wing::DecapsulationKey, + kemeleon_byte: u8, + elligator2_byte: u8, + pub_key: EncapsulationKey, -struct HybridKey { - x25519: x25519_dalek::StaticSecret, - mlkem: DecapsulationKey, - pub_key: PublicKey, + /// Keeping this around because we have extra randomness bytes that we need + /// to keep track of for both elligator2 and kemeleon. -_- + byteformat: [u8; PRIVKEY_LEN + PUBKEY_LEN], } #[derive(Clone, PartialEq)] -pub struct PublicKey { - x25519: x25519_dalek::PublicKey, - mlkem: EncapsulationKey, - pub_key: [u8; PUBKEY_LEN], +pub struct EncapsulationKey { + encap: x_wing::EncapsulationKey, + /// public key encoded as bytes using obfuscating encodings. + pub_key_obfs: [u8; PUBKEY_LEN], } -impl StaticSecret { - pub fn random_from_rng(rng: &mut R) -> Self { - Self(HybridKey::new(rng)) +/// Generate a X-Wing key pair using the provided rng. +pub fn generate_key_pair(rng: &mut impl CryptoRngCore) -> (DecapsulationKey, EncapsulationKey) { + let (dk, ek) = x_wing::generate_key_pair(rng); + (dk, ek) +} + +pub struct Ciphertext(x_wing::Ciphertext); + +impl kem::Decapsulate for DecapsulationKey { + type Error = Error; + fn decapsulate( + &self, + encapsulated_key: &Ciphertext, + ) -> std::result::Result { + // self.decap.decapsulate(encapsulated_key).into() + todo!("out for lunch") } +} +impl kem::Encapsulate for EncapsulationKey { + type Error = Error; + fn encapsulate( + &self, + rng: &mut impl CryptoRngCore, + ) -> std::result::Result<(Ciphertext, SharedSecret), Self::Error> { + todo!("out of order") + } +} + +impl DecapsulationKey { // TODO: THIS NEEDS TESTED pub fn try_from_bytes(bytes: impl AsRef<[u8]>) -> Result { let buf = bytes.as_ref(); + if buf.len() < 32 { + return Err(Error::Crypto("malformed DecapsulationKey provided".into())); + } - let sk: [u8; X25519_PRIVKEY_LEN] = core::array::from_fn(|i| buf[i]); - let x25519 = x25519_dalek::StaticSecret::from(sk); - - let mlkem = DecapsulationKey::from_fips_bytes(&buf[X25519_PRIVKEY_LEN..]) - .map_err(|e| Error::EncodeError(e.into()))?; + let mut b = [0u8; 32]; + b.copy_from_slice(&buf[..32]); + let privkey = x_wing::DecapsulationKey::from(b); - let pubkey_buf: [u8; PUBKEY_LEN] = core::array::from_fn(|i| buf[PRIVKEY_LEN + i]); - let pub_key = PublicKey::try_from(pubkey_buf)?; + todo!("not implemented: back in an hour"); + // let sk: [u8; X25519_PRIVKEY_LEN] = core::array::from_fn(|i| buf[i]); + // let x25519 = x25519_dalek::StaticSecret::from(sk); - Ok(Self(HybridKey { - pub_key, - mlkem, - x25519, - })) - } + // let mlkem = DecapsulationKey::from_fips_bytes(&buf[X25519_PRIVKEY_LEN..]) + // .map_err(|e| Error::EncodeError(e.into()))?; - pub fn as_bytes(&self) -> [u8; PRIVKEY_LEN + PUBKEY_LEN] { - let mut out = [0u8; PRIVKEY_LEN + PUBKEY_LEN]; - out[..X25519_PRIVKEY_LEN].copy_from_slice(&self.0.x25519.to_bytes()[..]); - out[X25519_PRIVKEY_LEN..PRIVKEY_LEN].copy_from_slice(&self.0.mlkem.to_fips_bytes()[..]); - out[PRIVKEY_LEN..PRIVKEY_LEN + PUBKEY_LEN].copy_from_slice(&self.0.pub_key.as_bytes()); - out - } + // let pubkey_buf: [u8; PUBKEY_LEN] = core::array::from_fn(|i| buf[PRIVKEY_LEN + i]); + // let pub_key = EncapsulationKey::try_from(pubkey_buf)?; - pub fn with_pub<'a>(&'a self, pubkey: &'a PublicKey) -> KeyMix<'a> { - self.0.with_pub(pubkey) + // Ok(Self{ + // decap, + // kemeleon_byte, + // elligator2_byte, + // pub_key, + // byteformat, + // }) } - /// Hybrid Public Key Encryption (HPKE) handshake for ML-KEM1024 + X25519 - /// - /// This is a custom interface for now as there isn't an example interface that I am aware of. - /// (Read - this will likely change in the future) - pub fn hpke( - &self, - rng: &mut R, - pubkey: &PublicKey, - ) -> Result<(Ciphertext, SharedSecret)> { - self.with_pub(&pubkey) - .encapsulate(rng) - .map_err(|e| Error::Crypto(e.to_string())) + pub fn as_bytes(&self) -> [u8; PRIVKEY_LEN + PUBKEY_LEN] { + self.byteformat.clone() } } -impl core::fmt::Debug for PublicKey { +impl core::fmt::Debug for EncapsulationKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", hex::encode(self.as_bytes())) } } -impl PublicKey { +impl EncapsulationKey { pub fn as_bytes(&self) -> &[u8] { - &self.pub_key + &self.pub_key_obfs } } -impl From<&StaticSecret> for PublicKey { - fn from(value: &StaticSecret) -> Self { - value.0.public_key().clone() +impl From<&DecapsulationKey> for EncapsulationKey { + fn from(value: &DecapsulationKey) -> Self { + value.pub_key.clone() } } -impl TryFrom<&[u8]> for StaticSecret { +impl TryFrom<&[u8]> for DecapsulationKey { type Error = Error; fn try_from(value: &[u8]) -> Result { - StaticSecret::try_from_bytes(value) + DecapsulationKey::try_from_bytes(value) } } -impl TryFrom<&[u8]> for PublicKey { +impl TryFrom<&[u8]> for EncapsulationKey { type Error = Error; fn try_from(value: &[u8]) -> std::result::Result { if value.len() < PUBKEY_LEN { @@ -132,20 +135,19 @@ impl TryFrom<&[u8]> for PublicKey { let mut x25519 = [0u8; X25519_PUBKEY_LEN]; x25519.copy_from_slice(&value[..X25519_PUBKEY_LEN]); - let mlkem = EncapsulationKey::try_from_bytes(&value[X25519_PUBKEY_LEN..]) + let mlkem = kemeleon::EncapsulationKey::try_from_bytes(&value[X25519_PUBKEY_LEN..]) .map_err(|e| Error::EncodeError(e.into()))?; let mut pub_key = [0u8; PUBKEY_LEN]; pub_key.copy_from_slice(&value[..PUBKEY_LEN]); Ok(Self { - x25519: x25519.into(), - mlkem, - pub_key, + encap, + pub_key_obfs, }) } } -impl TryFrom<[u8; PUBKEY_LEN]> for PublicKey { +impl TryFrom<[u8; PUBKEY_LEN]> for EncapsulationKey { type Error = Error; fn try_from(value: [u8; PUBKEY_LEN]) -> Result { Self::try_from(&value[..]) @@ -153,7 +155,7 @@ impl TryFrom<[u8; PUBKEY_LEN]> for PublicKey { } pub struct SharedSecret { - x25519: x25519_dalek::SharedSecret, + shared_secret: x_wing::SharedSecret, x25519_raw: [u8; 32], mlkem: [u8; 32], } @@ -170,12 +172,6 @@ impl SharedSecret { pub fn as_bytes(&self) -> &[u8] { unsafe { std::slice::from_raw_parts(self.x25519_raw.as_ptr(), 64) } } - - /// Ensure in constant-time that the x25519 portion of this shared secret did not result - /// from a key exchange with non-contributory behaviour. - pub fn was_contributory(&self) -> bool { - self.x25519.was_contributory() - } } impl core::fmt::Debug for SharedSecret { @@ -189,96 +185,6 @@ impl core::fmt::Debug for SharedSecret { } } -impl HybridKey { - fn new(rng: &mut R) -> Self { - let (dk, ek) = kemeleon::MlKem1024::generate(rng); - let x25519 = x25519_dalek::StaticSecret::random_from_rng(rng); - let x25519_pub = x25519_dalek::PublicKey::from(&x25519); - let mut pub_key = [0u8; PUBKEY_LEN]; - pub_key[..X25519_PUBKEY_LEN].copy_from_slice(x25519_pub.as_bytes()); - pub_key[X25519_PUBKEY_LEN..].copy_from_slice(&ek.as_bytes()); - - Self { - pub_key: PublicKey { - x25519: x25519_dalek::PublicKey::from(&x25519), - mlkem: ek, - pub_key, - }, - mlkem: dk, - x25519, - } - } - - fn public_key(&self) -> &PublicKey { - &self.pub_key - } - - fn with_pub<'a>(&'a self, pubkey: &'a PublicKey) -> KeyMix<'a> { - KeyMix { - local_private: self, - remote_public: pubkey, - } - } -} - -pub struct KeyMix<'a> { - local_private: &'a HybridKey, - remote_public: &'a PublicKey, -} - -impl Encapsulate for KeyMix<'_> { - type Error = EncodeError; - - // Diffie Helman / Encapsulate - fn encapsulate( - &self, - rng: &mut impl CryptoRngCore, - ) -> std::result::Result<(Ciphertext, SharedSecret), Self::Error> { - let (ciphertext, local_ss_mlkem) = self.remote_public.mlkem.encapsulate(rng).unwrap(); - let local_ss_x25519 = self - .local_private - .x25519 - .diffie_hellman(&self.remote_public.x25519); - let ss = SharedSecret { - x25519_raw: (&local_ss_x25519).to_bytes(), - mlkem: local_ss_mlkem.into(), - x25519: local_ss_x25519, - }; - let mut ct = x25519_dalek::PublicKey::from(&self.local_private.x25519) - .as_bytes() - .to_vec(); - ct.append(&mut ciphertext.as_bytes().to_vec()); - Ok((ct, ss)) - } -} - -pub type Ciphertext = Vec; - -impl Decapsulate for HybridKey { - type Error = EncodeError; - - // Required method - fn decapsulate( - &self, - encapsulated_key: &Ciphertext, - ) -> std::result::Result { - let arr = kemeleon::Ciphertext::try_from(&encapsulated_key[32..])?; - let local_ss_mlkem = self.mlkem.decapsulate(&arr)?; - - let mut remote_public = [0u8; 32]; - remote_public[..32].copy_from_slice(&encapsulated_key[..32]); - let local_ss_x25519 = self - .x25519 - .diffie_hellman(&x25519_dalek::PublicKey::from(remote_public)); - - Ok(SharedSecret { - x25519_raw: (&local_ss_x25519).to_bytes(), - mlkem: local_ss_mlkem.into(), - x25519: local_ss_x25519, - }) - } -} - #[cfg(test)] mod test { use super::*; @@ -287,18 +193,23 @@ mod test { #[test] fn example_lib_usage() { let rng = &mut rand::thread_rng(); - let alice_priv_key = HybridKey::new(rng); - let alice_pub = alice_priv_key.public_key(); + let (dk_alice, ek_alice) = generate_key_pair(rng); - let bob_priv_key = HybridKey::new(rng); - let (ct, bob_ss) = bob_priv_key.with_pub(alice_pub).encapsulate(rng).unwrap(); + let (dk_bob, ek_bob) = generate_key_pair(rng); + let (ct, bob_ss) = ek_alice + .encapsulate(rng) + .expect("failed to encapsulate a secret"); - let alice_ss = alice_priv_key.decapsulate(&ct).unwrap(); + let alice_ss = dk_alice.decapsulate(&ct).unwrap(); assert_eq!(alice_ss, bob_ss); } #[test] - fn it_works() { + /// Make sure that serializing and then deserializing each type of object works as expected. + fn ser_de() {} + + #[test] + fn proof_of_concept() { let mut rng = rand::thread_rng(); // --- Generate Keypair (Alice) --- diff --git a/crates/o5/src/framing/handshake.rs b/crates/o5/src/framing/handshake.rs index e725a41..5274944 100644 --- a/crates/o5/src/framing/handshake.rs +++ b/crates/o5/src/framing/handshake.rs @@ -108,7 +108,10 @@ pub struct ClientHandshakeMessage<'a> { } impl<'a> ClientHandshakeMessage<'a> { - pub(crate) fn new(client_session_pubkey: SessionPublicKey, hs_materials: &'a CHSMaterials) -> Self { + pub(crate) fn new( + client_session_pubkey: SessionPublicKey, + hs_materials: &'a CHSMaterials, + ) -> Self { Self { hs_materials, client_session_pubkey, diff --git a/crates/o5/src/handshake.rs b/crates/o5/src/handshake.rs index 1e8a630..6f864cf 100644 --- a/crates/o5/src/handshake.rs +++ b/crates/o5/src/handshake.rs @@ -212,7 +212,7 @@ fn kdf_msgkdf( msg_kdf.write(&xb.as_bytes())?; msg_kdf.write(&relay_public.id)?; msg_kdf.write(&client_public.as_bytes())?; - msg_kdf.write(&relay_public.pk.as_bytes())?; + msg_kdf.write(&relay_public.ek.as_bytes())?; msg_kdf.write(PROTOID)?; msg_kdf.write(&Encap(verification))?; let mut r = msg_kdf.take().finalize_xof(); @@ -226,7 +226,7 @@ fn kdf_msgkdf( mac.write(&T_MSGMAC)?; mac.write(&Encap(&mac_key[..]))?; mac.write(&relay_public.id)?; - mac.write(&relay_public.pk.as_bytes())?; + mac.write(&relay_public.ek.as_bytes())?; mac.write(&client_public.as_bytes())?; } @@ -274,7 +274,7 @@ mod test { use crate::common::ntor_arti::{ ClientHandshake, ClientHandshakeComplete, KeyGenerator, ServerHandshake, }; - use crate::common::xwing::{PublicKey, StaticSecret}; + use crate::common::xwing::{DecapsulationKey, EncapsulationKey}; use crate::constants::{NODE_ID_LENGTH, SEED_LENGTH}; use crate::Server; @@ -403,11 +403,11 @@ mod test { let id = <[u8; NODE_ID_LENGTH]>::from_hex(KEYS[0].id).unwrap(); let x = hex::decode(KEYS[0].x).expect("failed to unhex x"); let y = hex::decode(KEYS[0].y).expect("failed to unhex y"); - let b = StaticSecret::try_from(&b[..]).expect("failed to parse b"); - let B = PublicKey::from(&b); + let b = DecapsulationKey::try_from(&b[..]).expect("failed to parse b"); + let B = EncapsulationKey::from(&b); let x = SessionSecretKey::try_from(&x[..]).expect("failed_to parse x"); //let X = (&x).into(); - let y = StaticSecret::try_from(&y[..]).expect("failed to parse y"); + let y = DecapsulationKey::try_from(&y[..]).expect("failed to parse y"); let client_message = hex!("68656c6c6f20776f726c64"); let verification = hex!("78797a7a79"); @@ -465,22 +465,19 @@ mod test { fn xwing_3way_handshake_flow() { let mut rng = rand::thread_rng(); // long-term server id and keys - let server_id_keys = StaticSecret::random_from_rng(&mut rng); - let _server_id_pub = PublicKey::from(&server_id_keys); + let (dk_si, ek_si) = xwing::generate_key_pair(&mut rng); // let server_id = ID::new(); // client open session, generating the associated ephemeral keys - let client_session = StaticSecret::random_from_rng(&mut rng); - - // client sends xwing session pubkey(s) - let _cpk = PublicKey::from(&client_session); + // and sends xwing session pubkey using an obfuscated encoding + // along with an obfuscated ciphertext containing an initial shared secret + let (dk_cs, ek_cs) = xwing::generate_key_pair(&mut rng); // server computes xwing combined shared secret - let _server_session = StaticSecret::random_from_rng(&mut rng); + let (dk_ss, ek_ss) = xwing::generate_key_pair(&mut rng); // let server_hs_res = server_handshake(&server_session, &cpk, &server_id_keys, &server_id); // server sends mlkemx25519 session pubkey(s) - let _spk = PublicKey::from(&client_session); // // client computes xwing combined shared secret // let client_hs_res = client_handshake(&client_session, &spk, &server_id_pub, &server_id); diff --git a/crates/o5/src/handshake/client.rs b/crates/o5/src/handshake/client.rs index 991c294..88d2877 100644 --- a/crates/o5/src/handshake/client.rs +++ b/crates/o5/src/handshake/client.rs @@ -4,7 +4,7 @@ use crate::{ ntor_arti::{ ClientHandshake, ClientHandshakeComplete, ClientHandshakeMaterials, KeyGenerator, }, - xwing::SharedSecret, + xwing::{DecapsulationKey, SharedSecret}, }, constants::*, framing::handshake::ClientHandshakeMessage, @@ -53,8 +53,8 @@ pub(crate) struct HandshakeState { } impl HandshakeState { - fn node_pubkey(&self) -> &xwing::PublicKey { - &self.materials.node_pubkey.pk + fn node_pubkey(&self) -> &xwing::EncapsulationKey { + &self.materials.node_pubkey.ek } fn node_id(&self) -> Ed25519Identity { @@ -174,8 +174,8 @@ pub(crate) fn client_handshake_ntor_v3( materials: HandshakeMaterials, verification: &[u8], ) -> EncodeResult<(HandshakeState, Vec)> { - let my_sk = SessionSecretKey::random_from_rng(rng); - client_handshake_ntor_v3_no_keygen(my_sk, materials, verification) + let (dk_session, _ek_session) = xwing::generate_key_pair(rng); + client_handshake_ntor_v3_no_keygen(dk_session, materials, verification) } /// As `client_handshake_ntor_v3`, but don't generate an ephemeral DH diff --git a/crates/o5/src/handshake/keys.rs b/crates/o5/src/handshake/keys.rs index ae85f06..56b7f97 100644 --- a/crates/o5/src/handshake/keys.rs +++ b/crates/o5/src/handshake/keys.rs @@ -3,7 +3,7 @@ use crate::{ common::{ ntor_arti::{KeyGenerator, SessionID, SessionIdentifier}, // kdf::{Kdf, Ntor1Kdf}, - xwing::{self, Ciphertext, PublicKey, SharedSecret, StaticSecret}, + xwing::{self, Ciphertext, DecapsulationKey, EncapsulationKey, SharedSecret}, }, constants::*, framing::{O5Codec, KEY_MATERIAL_LENGTH}, @@ -30,7 +30,7 @@ pub struct IdentityPublicKey { /// The relay's identity. pub(crate) id: Ed25519Identity, /// The relay's onion key. - pub(crate) pk: PublicKey, + pub(crate) ek: EncapsulationKey, } impl From<&IdentitySecretKey> for IdentityPublicKey { @@ -44,9 +44,9 @@ impl IdentityPublicKey { const CERT_SUFFIX: &'static str = "=="; /// Construct a new IdentityPublicKey from its components. #[allow(unused)] - pub(crate) fn new(pk: [u8; xwing::PUBKEY_LEN], id: [u8; NODE_ID_LENGTH]) -> Result { + pub(crate) fn new(ek_bytes: [u8; xwing::PUBKEY_LEN], id: [u8; NODE_ID_LENGTH]) -> Result { Ok(Self { - pk: pk.try_into()?, + ek: ek_bytes.try_into()?, id: id.into(), }) } @@ -64,8 +64,8 @@ impl std::str::FromStr for IdentityPublicKey { return Err(format!("cert length {} is invalid", decoded.len()).into()); } let id: [u8; NODE_ID_LENGTH] = decoded[..NODE_ID_LENGTH].try_into()?; - let pk: [u8; NODE_PUBKEY_LENGTH] = decoded[NODE_ID_LENGTH..].try_into()?; - IdentityPublicKey::new(pk, id) + let ek: [u8; NODE_PUBKEY_LENGTH] = decoded[NODE_ID_LENGTH..].try_into()?; + IdentityPublicKey::new(ek, id) } } @@ -73,7 +73,7 @@ impl std::str::FromStr for IdentityPublicKey { impl std::string::ToString for IdentityPublicKey { fn to_string(&self) -> String { let mut s = Vec::from(self.id.as_bytes()); - s.extend(self.pk.as_bytes()); + s.extend(self.ek.as_bytes()); STANDARD_NO_PAD.encode(s) } } @@ -89,17 +89,17 @@ pub struct IdentitySecretKey { /// The relay's public key information pub(crate) pk: IdentityPublicKey, /// The secret onion key. - pub(super) sk: StaticSecret, + pub(super) sk: DecapsulationKey, } impl IdentitySecretKey { /// Construct a new IdentitySecretKey from its components. #[allow(unused)] - pub(crate) fn new(sk: StaticSecret, id: Ed25519Identity) -> Self { + pub(crate) fn new(sk: DecapsulationKey, id: Ed25519Identity) -> Self { Self { pk: IdentityPublicKey { id, - pk: PublicKey::from(&sk), + ek: EncapsulationKey::from(&sk), }, sk, } @@ -113,7 +113,7 @@ impl IdentitySecretKey { let mut id = [0u8; NODE_ID_LENGTH]; id.copy_from_slice(&buf[..NODE_ID_LENGTH]); - let sk = StaticSecret::try_from_bytes(&buf[NODE_ID_LENGTH..])?; + let sk = DecapsulationKey::try_from_bytes(&buf[NODE_ID_LENGTH..])?; Ok(Self::new(sk, id.into())) } @@ -122,30 +122,16 @@ impl IdentitySecretKey { let mut id = [0_u8; NODE_ID_LENGTH]; // Random bytes will work for testing, but aren't necessarily actually a valid id. rng.fill_bytes(&mut id); - let sk = StaticSecret::random_from_rng(rng); - Self::new(sk, id.into()) + // let sk = DecapsulationKey::random_from_rng(rng); + let (dk, ek) = xwing::generate_key_pair(rng); + Self::new(dk, id.into()) } /// Checks whether `id` and `pk` match this secret key. /// /// Used to perform a constant-time secret key lookup. - pub(crate) fn matches(&self, id: Ed25519Identity, pk: PublicKey) -> Choice { - id.as_bytes().ct_eq(self.pk.id.as_bytes()) & pk.as_bytes().ct_eq(self.pk.pk.as_bytes()) - } - - /// Hybrid Public Key Encryption (HPKE) handshake for ML-KEM1024 + X25519 - /// - /// This is a custom interface for now as there isn't an example interface that I am aware of. - /// (Read - this will likely change in the future) - pub fn hpke( - &self, - rng: &mut R, - pubkey: &IdentityPublicKey, - ) -> Result<(Ciphertext, SharedSecret)> { - self.sk - .with_pub(&pubkey.pk) - .encapsulate(rng) - .map_err(|e| Error::Crypto(e.to_string())) + pub(crate) fn matches(&self, id: Ed25519Identity, ek: EncapsulationKey) -> Choice { + id.as_bytes().ct_eq(self.pk.id.as_bytes()) & ek.as_bytes().ct_eq(self.pk.ek.as_bytes()) } } @@ -156,15 +142,15 @@ impl TryFrom<&[u8]> for IdentitySecretKey { } } -impl Into for &IdentityPublicKey { - fn into(self) -> xwing::PublicKey { - self.pk.clone() +impl Into for &IdentityPublicKey { + fn into(self) -> xwing::EncapsulationKey { + self.ek.clone() } } -impl Into for &IdentitySecretKey { - fn into(self) -> xwing::PublicKey { - self.pk.pk.clone() +impl Into for &IdentitySecretKey { + fn into(self) -> xwing::EncapsulationKey { + self.pk.ek.clone() } } diff --git a/crates/o5/src/handshake/server.rs b/crates/o5/src/handshake/server.rs index f75da4d..21855ae 100644 --- a/crates/o5/src/handshake/server.rs +++ b/crates/o5/src/handshake/server.rs @@ -31,7 +31,7 @@ pub(crate) struct HandshakeMaterials { impl<'a> HandshakeMaterials { pub fn get_hmac<'b>(&self, identity_keys: &'b IdentitySecretKey) -> Hmac { - let mut key = identity_keys.pk.pk.as_bytes().to_vec(); + let mut key = identity_keys.pk.ek.as_bytes().to_vec(); key.append(&mut identity_keys.pk.id.as_bytes().to_vec()); Hmac::::new_from_slice(&key[..]).unwrap() } @@ -96,8 +96,8 @@ pub(crate) fn server_handshake_ntor_v3( keys: &IdentitySecretKey, verification: &[u8], ) -> RelayHandshakeResult<(Vec, NtorV3XofReader)> { - let secret_key_y = SessionSecretKey::random_from_rng(rng); - server_handshake_ntor_v3_no_keygen(rng, reply_fn, &secret_key_y, message, keys, verification) + let (dk_session, _ek_session) = xwing::generate_key_pair(rng); + server_handshake_ntor_v3_no_keygen(rng, reply_fn, &dk_session, message, keys, verification) } /// As `server_handshake_ntor_v3`, but take a secret key instead of an RNG. diff --git a/crates/o5/src/lib.rs b/crates/o5/src/lib.rs index 88816ec..940bde9 100644 --- a/crates/o5/src/lib.rs +++ b/crates/o5/src/lib.rs @@ -1,5 +1,4 @@ #![doc = include_str!("../README.md")] - // #![warn(missing_docs)] #![allow(unused)] diff --git a/crates/o5/src/pt.rs b/crates/o5/src/pt.rs index cbfa976..308da40 100644 --- a/crates/o5/src/pt.rs +++ b/crates/o5/src/pt.rs @@ -74,7 +74,7 @@ where trace!( "node_pubkey: {}, node_id: {}", - hex::encode(self.identity_keys.pk.pk.as_bytes()), + hex::encode(self.identity_keys.pk.ek.as_bytes()), hex::encode(self.identity_keys.pk.id.as_bytes()), ); Ok(self) diff --git a/crates/o5/src/sessions.rs b/crates/o5/src/sessions.rs index 0ae9a17..16623e4 100644 --- a/crates/o5/src/sessions.rs +++ b/crates/o5/src/sessions.rs @@ -13,10 +13,10 @@ mod server; pub(crate) use server::ServerSession; /// Ephermeral single use session secret key type -pub type SessionSecretKey = xwing::StaticSecret; +pub type SessionSecretKey = xwing::DecapsulationKey; /// Public key type associated with SessionSecretKey. -pub type SessionPublicKey = xwing::PublicKey; +pub type SessionPublicKey = xwing::EncapsulationKey; impl Readable for SessionPublicKey { fn take_from(_b: &mut tor_bytes::Reader<'_>) -> tor_bytes::Result { diff --git a/crates/o5/src/sessions/client.rs b/crates/o5/src/sessions/client.rs index 51ff584..71ac8e9 100644 --- a/crates/o5/src/sessions/client.rs +++ b/crates/o5/src/sessions/client.rs @@ -275,7 +275,7 @@ impl std::fmt::Debug for ClientSession { f, "[ id:{}, ident_pk:{}, epoch_hr:{} ]", hex::encode(self.node_pubkey.id.as_bytes()), - hex::encode(self.node_pubkey.pk.as_bytes()), + hex::encode(self.node_pubkey.ek.as_bytes()), self.epoch_hour, ) } diff --git a/crates/ptrs/src/lib.rs b/crates/ptrs/src/lib.rs index d26eeef..5f1e54d 100644 --- a/crates/ptrs/src/lib.rs +++ b/crates/ptrs/src/lib.rs @@ -133,7 +133,6 @@ where /// this server based on the configuration of the server when this method /// is called. fn get_client_params(&self) -> String; - } /// Server Transport builder interface From 27eeece29a34aab40fc163ee64ab218f716c6daf Mon Sep 17 00:00:00 2001 From: jmwample <8297368+jmwample@users.noreply.github.com> Date: Mon, 21 Oct 2024 07:51:20 -0600 Subject: [PATCH 7/7] save state. testing very incomplete --- crates/o5/src/common/xwing.rs | 105 ++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/crates/o5/src/common/xwing.rs b/crates/o5/src/common/xwing.rs index 7e27de8..341947a 100644 --- a/crates/o5/src/common/xwing.rs +++ b/crates/o5/src/common/xwing.rs @@ -3,10 +3,14 @@ //! todo! use kem::{Decapsulate, Encapsulate}; -use kemeleon::{Encode, OKemCore}; +use kemeleon::{ + Encode, EncodingSize, KemeleonByteArraySize, KemeleonEncodingSize, OKemCore, Transcode, +}; +use ml_kem::{EncodedSizeUser, MlKem768, MlKem768Params}; use rand::{CryptoRng, RngCore}; use rand_core::CryptoRngCore; use subtle::ConstantTimeEq; +use typenum::Unsigned; use crate::{Error, Result}; @@ -14,15 +18,17 @@ pub(crate) use kemeleon::EncodeError; pub(crate) const X25519_PUBKEY_LEN: usize = 32; pub(crate) const X25519_PRIVKEY_LEN: usize = 32; -pub(crate) const MLKEM1024_PUBKEY_LEN: usize = 1530; -pub(crate) const PUBKEY_LEN: usize = MLKEM1024_PUBKEY_LEN + X25519_PUBKEY_LEN; -pub(crate) const PRIVKEY_LEN: usize = 1; +pub(crate) const PUBKEY_LEN: usize = + ::ENCODED_EK_SIZE::USIZE + X25519_PUBKEY_LEN; +pub(crate) const PRIVKEY_LEN: usize = x_wing::DECAPSULATION_KEY_SIZE; +pub(crate) const CANONICAL_PUBKEY_LEN: usize = x_wing::ENCAPSULATION_KEY_SIZE; +pub(crate) const CANONICAL_PRIVKEY_LEN: usize = x_wing::DECAPSULATION_KEY_SIZE; pub struct DecapsulationKey { - decap: x_wing::DecapsulationKey, + dk: x_wing::DecapsulationKey, kemeleon_byte: u8, elligator2_byte: u8, - pub_key: EncapsulationKey, + ek: EncapsulationKey, /// Keeping this around because we have extra randomness bytes that we need /// to keep track of for both elligator2 and kemeleon. -_- @@ -31,7 +37,7 @@ pub struct DecapsulationKey { #[derive(Clone, PartialEq)] pub struct EncapsulationKey { - encap: x_wing::EncapsulationKey, + ek: x_wing::EncapsulationKey, /// public key encoded as bytes using obfuscating encodings. pub_key_obfs: [u8; PUBKEY_LEN], } @@ -39,7 +45,19 @@ pub struct EncapsulationKey { /// Generate a X-Wing key pair using the provided rng. pub fn generate_key_pair(rng: &mut impl CryptoRngCore) -> (DecapsulationKey, EncapsulationKey) { let (dk, ek) = x_wing::generate_key_pair(rng); - (dk, ek) + let extra = rng.next_u32().to_be_bytes(); + let encap = EncapsulationKey { + ek, + pub_key_obfs: [0u8; PUBKEY_LEN], + }; + let decap = DecapsulationKey { + dk, + kemeleon_byte: extra[0], + elligator2_byte: extra[1], + ek: encap.clone(), + byteformat: [0u8; PUBKEY_LEN + PRIVKEY_LEN], + }; + (decap, encap) } pub struct Ciphertext(x_wing::Ciphertext); @@ -96,26 +114,46 @@ impl DecapsulationKey { // }) } - pub fn as_bytes(&self) -> [u8; PRIVKEY_LEN + PUBKEY_LEN] { - self.byteformat.clone() + /// Return byte representation in xwing standard format. + pub fn to_bytes_canonical(&self) -> &[u8; x_wing::DECAPSULATION_KEY_SIZE] { + self.dk.as_bytes() } -} -impl core::fmt::Debug for EncapsulationKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", hex::encode(self.as_bytes())) + /// Return byte representation in obfuscated encoded format. + pub fn as_bytes(&self) -> &[u8; PRIVKEY_LEN + PUBKEY_LEN] { + &self.byteformat } } impl EncapsulationKey { - pub fn as_bytes(&self) -> &[u8] { + /// Return byte representation in xwing standard format. + pub fn to_bytes_canonical(&self) -> [u8; x_wing::ENCAPSULATION_KEY_SIZE] { + self.ek.as_bytes() + } + + /// Return byte representation in obfuscated encoded format. + pub fn as_bytes(&self) -> &[u8; PUBKEY_LEN] { &self.pub_key_obfs } + + /// Return byte representation in obfuscated encoded format. + pub fn from_canonical(bytes: &[u8; x_wing::ENCAPSULATION_KEY_SIZE]) -> Self { + Self { + ek: x_wing::EncapsulationKey::from(bytes), + pub_key_obfs: [0u8; PUBKEY_LEN], + } + } +} + +impl core::fmt::Debug for EncapsulationKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.as_bytes())) + } } impl From<&DecapsulationKey> for EncapsulationKey { fn from(value: &DecapsulationKey) -> Self { - value.pub_key.clone() + value.ek.clone() } } @@ -132,17 +170,20 @@ impl TryFrom<&[u8]> for EncapsulationKey { if value.len() < PUBKEY_LEN { return Err(Error::Crypto("bad publickey".into())); } - let mut x25519 = [0u8; X25519_PUBKEY_LEN]; - x25519.copy_from_slice(&value[..X25519_PUBKEY_LEN]); - let mlkem = kemeleon::EncapsulationKey::try_from_bytes(&value[X25519_PUBKEY_LEN..]) - .map_err(|e| Error::EncodeError(e.into()))?; + let mlkem = + kemeleon::EncapsulationKey::::try_from_bytes(&value[X25519_PUBKEY_LEN..]) + .map_err(|e| Error::EncodeError(e.into()))?; + + let mut pub_key = [0u8; x_wing::ENCAPSULATION_KEY_SIZE]; + pub_key[..X25519_PUBKEY_LEN].copy_from_slice(&value[..X25519_PUBKEY_LEN]); + pub_key[X25519_PUBKEY_LEN..].copy_from_slice(&mlkem.as_fips().as_bytes()[..]); + + let ek = x_wing::EncapsulationKey::from(&pub_key); - let mut pub_key = [0u8; PUBKEY_LEN]; - pub_key.copy_from_slice(&value[..PUBKEY_LEN]); Ok(Self { - encap, - pub_key_obfs, + ek, + pub_key_obfs: [0u8; PUBKEY_LEN], // TODO }) } } @@ -206,7 +247,21 @@ mod test { #[test] /// Make sure that serializing and then deserializing each type of object works as expected. - fn ser_de() {} + fn ser_de() { + let (dk, ek) = generate_key_pair(&mut rand::thread_rng()); + let dk_bytes = dk.to_bytes_canonical(); + let ek_bytes = ek.to_bytes_canonical(); + + let ek_encoded = ek.as_bytes(); + + // --- + + let ek_r1 = EncapsulationKey::from_canonical(&ek_bytes); + let ek_r2 = EncapsulationKey::try_from(&ek_encoded[..]).expect(""); + + assert_eq!(ek_r1, ek); + assert_eq!(ek_r2, ek); + } #[test] fn proof_of_concept() {