Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 195 additions & 173 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
[package]
name = "ntor"
version = "0.1.0"
version = "0.1.2"
edition = "2024"

[dependencies]
ring = "0.17.8"
aes-gcm = "0.10"
bincode = "2.0.1"
curve25519-dalek = "4.1.1"
getrandom = { version = "0.2", features = ["js"] }
hex = "0.4.3"
hmac = "0.12"
log = "0.4.26"
serde = { version = "1.0.219", features = ["derive"] }
sha2 = "0.10"
curve25519-dalek = "4.1.1"
x25519-dalek = {version="^2.0.1", features = ["static_secrets"] }
rand_core = {version="0.6.4", features = ["std"]}
rand = "0.9.1"

x25519-dalek = { version = "^2.0.1", features = ["static_secrets"] }
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
# ntor
An Simple Implementation of the nTOR Protocol in Rust

## Generate ReverseProxy Ed25519 key-pair and certificate

```shell
cd python && python3 generate_ed25519_cert.pem -t [hex|dec|utf8] -v [value] -s [ntor_server_id]
```

Example:
```
cd python && python3 generate_ed25519_cert.py -t utf8 -v "this is 32-byte nTorStaticSecret" -s ReverseProxyServer
```
113 changes: 113 additions & 0 deletions python/generate_ed25519_cert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives import serialization, hashes
from cryptography import x509
from cryptography.x509.oid import NameOID
import datetime
import sys
import argparse

def parse_input(input_type, value):
print("value: '" + value + "'")
if input_type == "hex":
try:
key_bytes = bytes.fromhex(value)
except ValueError:
raise ValueError("Invalid hex input.")
elif input_type == "dec":
try:
key_bytes = bytes(int(b) for b in value.split(','))
except ValueError:
raise ValueError("Invalid decimal input. Use comma-separated values.")
elif input_type == "utf8":
key_bytes = value.encode("utf-8")
print("key_bytes:", key_bytes)
print("hex:", key_bytes.hex())
else:
raise ValueError("Input type must be one of: hex, dec, utf8")

if len(key_bytes) != 32:
raise ValueError(f"Input must be exactly 32 bytes, got {len(key_bytes)} bytes.")

return key_bytes


def generate_certificate(public_key, private_key, subject_str):
issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, u"reverse_proxy.com"),
])
subject = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, subject_str),
])

# x25519 key cannot be certificate signing key
signing_key = ed25519.Ed25519PrivateKey.generate()

with open("ed25519_private_key.pem", "wb") as f:
f.write(signing_key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()
))

cert = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.datetime.utcnow())
.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
.sign(private_key=signing_key, algorithm=None) # Ed25519: algorithm=None
)

with open("ed25519_cert.pem", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))

print("✅ Certificate saved to ed25519_cert.pem")


def main():
parser = argparse.ArgumentParser(description="Generate Ed25519 public.pem from 32-byte input.")
parser.add_argument("-t", "--type", choices=["hex", "dec", "utf8"], required=True, help="Input type: hex, dec, utf8")
parser.add_argument("-v", "--value", required=True, help="Input value (string)")
parser.add_argument("-s", "--subject", help="This value is ntor 'server_id'")

args = parser.parse_args()

# Step 1: Define the 32-byte raw Ed25519 private key
try:
# raw_key = b"this is 32-byte nTorStaticSecret"
raw_key = parse_input(args.type, args.value)
except ValueError as e:
print(f"❌ Error: {e}")
return

# Step 2: Create the Ed25519 private key
# private_key = ed25519.Ed25519PrivateKey.from_private_bytes(raw_key)
# public_key = private_key.public_key
static_private_key = x25519.X25519PrivateKey.from_private_bytes(raw_key)
static_public_key = static_private_key.public_key()
print("public_key", list(static_public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)))

with open("x25519_public_key.pem", "wb") as f:
pem = static_public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
f.write(pem)

with open("x25519_private_key.pem", "wb") as f:
f.write(static_private_key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()
))

generate_certificate(static_public_key, static_private_key, args.subject)

if __name__ == "__main__":
main()
125 changes: 125 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use crate::common::{
InitSessionMessage, InitSessionResponse, NTorCertificate, NTorParty, PrivatePublicKeyPair,
};
use crate::helpers::generate_private_public_key_pair;
use hmac::digest::Digest;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use x25519_dalek::PublicKey;

#[derive(Clone)]
pub struct NTorClient {
ephemeral_key_pair: PrivatePublicKeyPair,
pub(crate) shared_secret: Option<Vec<u8>>,
}

impl NTorClient {
pub fn new() -> Self {
Self {
ephemeral_key_pair: PrivatePublicKeyPair {
private_key: None,
public_key: PublicKey::from([0; 32]),
},
shared_secret: None,
}
}

pub fn initialise_session(&mut self) -> InitSessionMessage {
self.ephemeral_key_pair = generate_private_public_key_pair();

InitSessionMessage {
client_ephemeral_public_key: self.ephemeral_key_pair.public_key,
}
}

// Steps 15 - 20 of the Goldberg 2012 paper.
pub fn handle_response_from_server(
&mut self,
server_certificate: &NTorCertificate,
msg: &InitSessionResponse,
) -> bool {
// Step 18: Compute the shared secret.
let mut buffer: Vec<u8> = Vec::new();

// ECDH Client private ephemeral * server static public key
let taken_private_key = self.ephemeral_key_pair.private_key.take().unwrap();
let mut ecdh_result_1 = taken_private_key
.diffie_hellman(&msg.server_ephemeral_public_key)
.to_bytes()
.to_vec();

buffer.append(&mut ecdh_result_1);

// ECDH Client private ephemeral * server ephemeral public Key
let mut ecdh_result_2 = taken_private_key
.diffie_hellman(&server_certificate.public_key)
.to_bytes()
.to_vec();

buffer.append(&mut ecdh_result_2);

// Server id
buffer.append(&mut server_certificate.server_id.as_bytes().to_vec());

// Client ephemeral public
buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec());

// Server ephemeral public
buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec());

// "ntor" string identifier
buffer.append(&mut "ntor".as_bytes().to_vec());

// Instantiate and run hashing function
let mut hasher = Sha256::new();
hasher.update(buffer);
let sha256_hash = hasher.finalize();
let sha256_hash: &[u8; 32] = match sha256_hash.as_slice().try_into() {
Ok(array_ref) => array_ref,
Err(_) => {
panic!("Invalid sha256 hash length");
}
};

let secret_key_prime = &sha256_hash[0..16];

let secret_key = &sha256_hash[16..];

// Step 19: Compute HMAC (t_b in the paper)

let mut buffer: Vec<u8> = Vec::new();
buffer.append(&mut server_certificate.server_id.as_bytes().to_vec());
buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec());
buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec());
buffer.append(&mut "ntor".as_bytes().to_vec());
buffer.append(&mut "server".as_bytes().to_vec());

let mut hmac_hash = Hmac::<Sha256>::new_from_slice(&buffer).unwrap();
hmac_hash.update(secret_key_prime);
let computed_t_b_hash = hmac_hash.finalize().into_bytes().to_vec();

// assert that computed_t_b_hash equals t_hash generated by server
if computed_t_b_hash == msg.t_b_hash {
self.shared_secret = Some(secret_key.to_vec());
return true;
}

false
}
}

impl Default for NTorClient {
fn default() -> Self {
Self::new()
}
}

impl NTorParty for NTorClient {
fn get_shared_secret(&self) -> Option<&[u8]> {
self.shared_secret.as_deref()
}

fn set_shared_secret(&mut self, shared_secret: Vec<u8>) {
self.shared_secret = Some(shared_secret);
}
}
Loading