diff --git a/Cargo.lock b/Cargo.lock index e8260a1..4992c6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,55 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "asn1" version = "0.16.2" @@ -112,6 +161,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "core-foundation" version = "0.9.4" @@ -536,6 +631,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -619,6 +720,34 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.2" @@ -1240,6 +1369,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1523,6 +1658,10 @@ dependencies = [ name = "zns-cli" version = "0.1.0" dependencies = [ + "base64", + "clap", + "num-bigint", + "num-traits", "zns", ] diff --git a/Cargo.toml b/Cargo.toml index 591f85f..19d7240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "zns", diff --git a/zns-cli/Cargo.toml b/zns-cli/Cargo.toml index 8502722..a7683c9 100644 --- a/zns-cli/Cargo.toml +++ b/zns-cli/Cargo.toml @@ -3,6 +3,12 @@ name = "zns-cli" version = "0.1.0" edition = "2021" +[dependencies] +clap = { version = "4.5.13", features = ["derive"] } +base64 = "0.22.0" +num-bigint = "0.4" +num-traits = "0.2" + [dependencies.zns] version = "0.1.0" diff --git a/zns-cli/src/main.rs b/zns-cli/src/main.rs index c89b901..b31ff26 100644 --- a/zns-cli/src/main.rs +++ b/zns-cli/src/main.rs @@ -1,3 +1,285 @@ -fn main() { - todo!(); +use base64::prelude::*; +use num_bigint::BigUint; +use num_traits::FromPrimitive; +use std::error::Error; +use std::fs::{self, File}; +use std::io::Write; +use std::str::from_utf8; +use zns::{errors::ZNSError, reader::Reader}; + +use clap::Parser; + +/// Simple program to greet a person +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Name of the person to greet + #[arg(short, long)] + key: String, + + /// Name of the person to greet + #[arg(short, long)] + username: String, +} + +pub trait KeyTransformer { + fn from_openssh(reader: &mut Reader) -> Result + where + Self: Sized; + + fn to_dnskey(&self, username: &str) -> (String, String); +} + +struct Ed25519KeyPair { + private_payload: [u8; 32], + public_payload: [u8; 32], +} + +struct RSAKeyPair { + modulus: Vec, + public_exponent: Vec, + private_exponent: Vec, + prime1: Vec, + prime2: Vec, + exponent1: Vec, + exponent2: Vec, + coefficient: Vec, +} + +enum KeyPair { + ED255519(Ed25519KeyPair), + RSA(RSAKeyPair), +} + +#[allow(dead_code)] +struct OpenSSHKey { + ciphername: String, + kdfname: String, + kdfoptions: String, + keypair: KeyPair, +} + +fn read_string(reader: &mut Reader) -> Result { + let length = reader.read_u32()?; + let data = reader.read(length as usize)?; + let result = from_utf8(&data).map_err(|e| ZNSError::Key { + message: format!("Wrong ciphername format: {}", e.to_string()), + })?; + Ok(result.to_owned()) +} + +fn read_bytes(reader: &mut Reader) -> Result, ZNSError> { + let length = reader.read_u32()?; + let data = reader.read(length as usize)?; + Ok(data) +} + +impl KeyTransformer for Ed25519KeyPair { + fn from_openssh(reader: &mut Reader) -> Result { + // public key parts + let length = reader.read_u32()?; + reader.read(length as usize)?; + + // private key payload + let length = reader.read_u32()?; + let data = reader.read(length as usize)?; + + let private_payload = data[0..32].try_into().unwrap(); + let public_payload = data[32..].try_into().unwrap(); + + Ok(Self { + public_payload, + private_payload, + }) + } + + fn to_dnskey(&self, username: &str) -> (String, String) { + let version: &str = "Private-key-format: v1.3"; + let algorithm: &str = "Algorithm: 15 (ED25519)"; + let private_key = format!( + "PrivateKey: {}", + BASE64_STANDARD.encode(self.private_payload) + ); + let private_encoded = format!("{version}\n{algorithm}\n{private_key}"); + + let public_key = BASE64_STANDARD.encode(self.public_payload); + let public_encoded = format!("{username}.users.zeus.gent. IN KEY 256 3 15 {public_key}"); + + (private_encoded, public_encoded) + } +} + +impl KeyTransformer for RSAKeyPair { + fn from_openssh(reader: &mut Reader) -> Result { + let mut modulus = read_bytes(reader)?; + + if modulus[0] == 0 { + // Remove first byte, it's a null byte for sign. + modulus.remove(0); + } + + let public_exponent = read_bytes(reader)?; + let private_exponent = read_bytes(reader)?; + let prime1 = read_bytes(reader)?; + let prime2 = read_bytes(reader)?; + let coefficient = read_bytes(reader)?; + + let d = BigUint::from_bytes_be(&private_exponent); + let p = BigUint::from_bytes_be(&prime1); + let q = BigUint::from_bytes_be(&prime2); + + let pm = &d % (&p - BigUint::from_u8(1).unwrap()); + let qm = &d % (&q - BigUint::from_u8(1).unwrap()); + + let exponent1 = pm.to_bytes_be(); + let exponent2 = qm.to_bytes_be(); + + Ok(Self { + modulus, + public_exponent, + private_exponent, + prime1, + prime2, + exponent1, + exponent2, + coefficient, + }) + } + + fn to_dnskey(&self, username: &str) -> (String, String) { + let modulus = BASE64_STANDARD.encode(&self.modulus); + let pubexponent = BASE64_STANDARD.encode(&self.public_exponent); + let privexponent = BASE64_STANDARD.encode(&self.private_exponent); + let prime1 = BASE64_STANDARD.encode(&self.prime1); + let prime2 = BASE64_STANDARD.encode(&self.prime2); + let exp1 = BASE64_STANDARD.encode(&self.exponent1); + let exp2 = BASE64_STANDARD.encode(&self.exponent2); + let coeff = BASE64_STANDARD.encode(&self.coefficient); + + let private_encoded = format!( + "Private-key-format: v1.3 +Algorithm: 10 (RSASHA512) +Modulus: {modulus} +PublicExponent: {pubexponent} +PrivateExponent: {privexponent} +Prime1: {prime1} +Prime2: {prime2} +Exponent1: {exp1} +Exponent2: {exp2} +Coefficient: {coeff} +" + ); + + let mut public_key: Vec = vec![]; + + public_key.push(self.public_exponent.len() as u8); + public_key.extend(&self.public_exponent); + public_key.extend(&self.modulus); + + let encoded_pub = BASE64_STANDARD.encode(&public_key); + + let public_encoded = format!("{username}.users.zeus.gent. IN KEY 256 3 10 {encoded_pub}"); + + (private_encoded, public_encoded) + } +} + +impl KeyTransformer for OpenSSHKey { + fn from_openssh(reader: &mut Reader) -> Result { + // Reference Material: https://coolaj86.com/articles/the-openssh-private-key-format/ + + let buf = reader.read(14)?; + let magic = from_utf8(&buf).map_err(|e| ZNSError::Key { + message: format!("Not valid ASCII: {}", e.to_string()), + })?; + + if magic != "openssh-key-v1" { + return Err(ZNSError::Key { + message: String::from("ssh key does not match ASCII magic openssh-key-v1"), + }); + } + + reader.read_u8()?; + + let ciphername = read_string(reader)?; + let kdfname = read_string(reader)?; + let kdfoptions = read_string(reader)?; + + // Amount of keypairs + let nkeys = reader.read_u32()?; + + if nkeys != 1 { + return Err(ZNSError::Key { + message: format!( + "Only private key file with one keypair is supported, not {} keys", + nkeys + ), + }); + } + + // public key + let length = reader.read_u32()?; + reader.read(length as usize)?; + + // size of remaining payload + reader.read_u32()?; + + // salt and rounds + reader.read(8)?; + + // public keytype + let keytype = read_string(reader)?; + + let keypair = match keytype.as_str() { + "ssh-ed25519" => Ok(KeyPair::ED255519(Ed25519KeyPair::from_openssh(reader)?)), + "ssh-rsa" => Ok(KeyPair::RSA(RSAKeyPair::from_openssh(reader)?)), + other => Err(ZNSError::Key { + message: format!("Invalid public keytype {}", other), + }), + }?; + + let length = reader.read_u32()?; + reader.read(length as usize)?; + + Ok(Self { + ciphername, + kdfname, + kdfoptions, + keypair, + }) + } + + fn to_dnskey(&self, username: &str) -> (String, String) { + match &self.keypair { + KeyPair::ED255519(keypair) => keypair.to_dnskey(username), + KeyPair::RSA(keypair) => keypair.to_dnskey(username), + } + } +} + +fn ssh_to_dnskey(file_content: String, username: &str) -> Result<(), Box> { + let bin = BASE64_STANDARD.decode(file_content.trim())?; + let mut reader = Reader::new(&bin); + let key = OpenSSHKey::from_openssh(&mut reader)?; + + let mut file_private = File::create("Kdns.private")?; + let mut file_public = File::create("Kdns.key")?; + + let (private, public) = key.to_dnskey(username); + file_private.write(private.as_bytes())?; + file_public.write(public.as_bytes())?; + + Ok(()) +} + +fn main() { + let args = Args::parse(); + + match fs::read_to_string(args.key) { + Ok(contents) => match ssh_to_dnskey(contents, &args.username) { + Ok(()) => println!("Success"), + Err(error) => eprintln!("{}", error), + }, + Err(error) => eprintln!("{}", error), + } } diff --git a/zns/src/errors.rs b/zns/src/errors.rs index 3179abc..1af0326 100644 --- a/zns/src/errors.rs +++ b/zns/src/errors.rs @@ -10,8 +10,8 @@ pub enum ZNSError { Database { message: String }, #[error("Reader Error: {message:?}")] Reader { message: String }, - #[error("PublicKey Error: {message:?}")] - PublicKey { message: String }, + #[error("Key Error: {message:?}")] + Key { message: String }, #[error("Reqwest error")] Reqwest(#[from] reqwest::Error), @@ -38,7 +38,7 @@ impl ZNSError { ZNSError::NotAuth { .. } => RCODE::NOTAUTH, ZNSError::NXDomain { .. } => RCODE::NXDOMAIN, ZNSError::NotImp { .. } => RCODE::NOTIMP, - ZNSError::Refused { .. } | ZNSError::PublicKey { .. } => RCODE::REFUSED, + ZNSError::Refused { .. } | ZNSError::Key { .. } => RCODE::REFUSED, } } } diff --git a/zns/src/handlers/update/authenticate.rs b/zns/src/handlers/update/authenticate.rs index 0e17a0b..c5c9d0b 100644 --- a/zns/src/handlers/update/authenticate.rs +++ b/zns/src/handlers/update/authenticate.rs @@ -18,6 +18,7 @@ pub async fn authenticate( connection: &mut PgConnection, ) -> Result { if zone.len() >= Config::get().authoritative_zone.len() { + //TODO: panic? subtract let username = &zone[zone.len() - Config::get().authoritative_zone.len() - 1]; let ssh_verified = validate_ssh(username, sig).await.is_ok_and(|b| b); diff --git a/zns/src/handlers/update/pubkeys/mod.rs b/zns/src/handlers/update/pubkeys/mod.rs index 95a26f4..9373294 100644 --- a/zns/src/handlers/update/pubkeys/mod.rs +++ b/zns/src/handlers/update/pubkeys/mod.rs @@ -17,7 +17,7 @@ pub trait PublicKey { fn verify_ssh_type(reader: &mut Reader, key_type: &str) -> Result<(), ZNSError> { let type_size = reader.read_i32()?; let read = reader.read(type_size as usize)?; - let algo_type = from_utf8(&read).map_err(|e| ZNSError::PublicKey { + let algo_type = from_utf8(&read).map_err(|e| ZNSError::Key { message: format!( "Could not convert type name bytes to string: {}", e.to_string() @@ -27,7 +27,7 @@ pub trait PublicKey { if algo_type == key_type { Ok(()) } else { - Err(ZNSError::PublicKey { + Err(ZNSError::Key { message: String::from("ssh key type does not match identifier"), }) } diff --git a/zns/src/handlers/update/pubkeys/rsa.rs b/zns/src/handlers/update/pubkeys/rsa.rs index 06f7966..8b4728b 100644 --- a/zns/src/handlers/update/pubkeys/rsa.rs +++ b/zns/src/handlers/update/pubkeys/rsa.rs @@ -51,14 +51,14 @@ impl PublicKey for RsaPublicKey { n: asn1::BigInt::new(&self.n), e: asn1::BigInt::new(&self.e), }) - .map_err(|e| ZNSError::PublicKey { + .map_err(|e| ZNSError::Key { message: format!("Verify Error: {}", e), })?; let signature_type = match algorithm { Algorithm::RSASHA512 => Ok(&signature::RSA_PKCS1_2048_8192_SHA512), Algorithm::RSASHA256 => Ok(&signature::RSA_PKCS1_2048_8192_SHA256), - _ => Err(ZNSError::PublicKey { + _ => Err(ZNSError::Key { message: String::from("RsaPublicKey: invalid verify algorithm"), }), }?; diff --git a/zns/src/lib.rs b/zns/src/lib.rs index 9f42eda..14a9512 100644 --- a/zns/src/lib.rs +++ b/zns/src/lib.rs @@ -1,7 +1,6 @@ mod db; mod handlers; mod message; -mod reader; mod structs; mod utils; @@ -9,3 +8,4 @@ pub mod config; pub mod errors; pub mod parser; pub mod resolver; +pub mod reader;