10
0
Fork 0
mirror of https://github.com/ZeusWPI/ZNS.git synced 2024-11-21 21:41:10 +01:00

add DNSKEY authentication support

This commit is contained in:
Xander Bil 2024-06-09 23:58:40 +02:00
parent 4469b6f0b5
commit c6437e6901
No known key found for this signature in database
GPG key ID: EC9706B54A278598
10 changed files with 138 additions and 83 deletions

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
errors::DatabaseError, errors::DatabaseError,
structs::{Class, Question, Type, RR}, structs::{Class, Type, RR},
}; };
use diesel::prelude::*; use diesel::prelude::*;
@ -99,17 +99,18 @@ pub async fn insert_into_database(rr: &RR) -> Result<(), DatabaseError> {
Ok(()) Ok(())
} }
pub async fn get_from_database(question: &Question) -> Result<Vec<RR>, DatabaseError> { pub async fn get_from_database(
name: &Vec<String>,
_type: Type,
class: Class,
) -> Result<Vec<RR>, DatabaseError> {
let db_connection = &mut establish_connection(); let db_connection = &mut establish_connection();
let records = Record::get( let records =
db_connection, Record::get(db_connection, name.join("."), _type.into(), class.into()).map_err(|e| {
question.qname.join("."), DatabaseError {
question.qtype.clone().into(), message: e.to_string(),
question.qclass.clone().into(), }
) })?;
.map_err(|e| DatabaseError {
message: e.to_string(),
})?;
Ok(records Ok(records
.into_iter() .into_iter()

View file

@ -4,7 +4,7 @@ use crate::structs::RCODE;
pub struct DNSError { pub struct DNSError {
pub message: String, pub message: String,
pub rcode: RCODE pub rcode: RCODE,
} }
impl fmt::Display for DNSError { impl fmt::Display for DNSError {
@ -27,7 +27,7 @@ impl fmt::Display for ParseError {
#[derive(Debug)] #[derive(Debug)]
pub struct DatabaseError { pub struct DatabaseError {
pub message: String pub message: String,
} }
impl fmt::Display for DatabaseError { impl fmt::Display for DatabaseError {
@ -77,7 +77,24 @@ where
fn from(value: E) -> Self { fn from(value: E) -> Self {
DNSError { DNSError {
message: value.into().to_string(), message: value.into().to_string(),
rcode: RCODE::FORMERR rcode: RCODE::FORMERR,
}
}
}
trait Supported {}
impl Supported for reqwest::Error {}
impl Supported for DatabaseError {}
impl<E> From<E> for AuthenticationError
where
E: Supported,
E: std::fmt::Display,
{
fn from(value: E) -> Self {
AuthenticationError {
message: value.to_string(),
} }
} }
} }

View file

@ -14,7 +14,12 @@ impl ResponseHandler for QueryHandler {
response.header.arcount = 0; //TODO: fix this, handle unknown class values response.header.arcount = 0; //TODO: fix this, handle unknown class values
for question in &message.question { for question in &message.question {
let answers = get_from_database(&question).await; let answers = get_from_database(
&question.qname,
question.qtype.clone(),
question.qclass.clone(),
)
.await;
match answers { match answers {
Ok(rrs) => { Ok(rrs) => {

View file

@ -1,33 +1,30 @@
use reqwest::Error; use reqwest::Error;
use crate::{config::Config, errors::AuthenticationError}; use crate::{
config::Config,
db::models::get_from_database,
errors::{AuthenticationError, DatabaseError},
parser::FromBytes,
reader::Reader,
structs::{Class, RRClass, RRType, Type},
};
use super::sig::{PublicKey, Sig}; use super::{dnskey::DNSKeyRData, sig::Sig};
type SSHKeys = Vec<String>; pub(super) async fn authenticate(
sig: &Sig,
type Result<T> = std::result::Result<T, AuthenticationError>; zone: &Vec<String>,
) -> Result<bool, AuthenticationError> {
pub(super) async fn authenticate(sig: &Sig, zone: &Vec<String>) -> Result<bool> {
if zone.len() >= 4 { if zone.len() >= 4 {
let username = &zone[zone.len() - 4]; // Should match: username.users.zeus.gent let username = &zone[zone.len() - 4]; // Should match: username.users.zeus.gent
let public_keys = get_keys(username).await.map_err(|e| AuthenticationError {
message: e.to_string(),
})?;
Ok(public_keys.iter().any(|key| { let ssh_verified = validate_ssh(username, sig).await?;
let key_split: Vec<&str> = key.split_ascii_whitespace().collect();
match key_split.len() { if ssh_verified {
3 => { Ok(true)
let key_encoded = key_split[1].to_string(); } else {
match key_split[0] { Ok(validate_dnskey(zone, sig).await?)
"ssh-ed25519" => sig.verify(PublicKey::ED25519(key_encoded)), }
_ => false,
}
}
_ => false,
}
}))
} else { } else {
Err(AuthenticationError { Err(AuthenticationError {
message: String::from("Invalid zone"), message: String::from("Invalid zone"),
@ -35,13 +32,38 @@ pub(super) async fn authenticate(sig: &Sig, zone: &Vec<String>) -> Result<bool>
} }
} }
async fn get_keys(username: &String) -> std::result::Result<SSHKeys, Error> { async fn validate_ssh(username: &String, sig: &Sig) -> Result<bool, Error> {
Ok(reqwest::get(format!( Ok(reqwest::get(format!(
"{}/users/keys/{}", "{}/users/keys/{}",
Config::get().zauth_url, Config::get().zauth_url,
username username
)) ))
.await? .await?
.json::<SSHKeys>() .json::<Vec<String>>()
.await?) .await?
.iter()
.any(|key| {
let key_split: Vec<&str> = key.split_ascii_whitespace().collect();
match key_split.len() {
3 => match key_split[0] {
"ssh-ed25519" => sig.verify_ssh_ed25519(key_split[1]),
_ => false,
},
_ => false,
}
}))
}
async fn validate_dnskey(zone: &Vec<String>, sig: &Sig) -> Result<bool, DatabaseError> {
Ok(
get_from_database(zone, Type::Type(RRType::DNSKEY), Class::Class(RRClass::IN))
.await?
.iter()
.any(|rr| {
let mut reader = Reader::new(&rr.rdata);
DNSKeyRData::from_bytes(&mut reader)
.map(|key| key.verify(sig))
.is_ok_and(|b| b)
}),
)
} }

View file

@ -0,0 +1,36 @@
use base64::prelude::*;
use crate::{errors::ParseError, parser::FromBytes, reader::Reader};
use super::sig::Sig;
/// https://datatracker.ietf.org/doc/html/rfc4034#section-2
#[derive(Debug)]
pub(super) struct DNSKeyRData {
pub flags: u16,
pub protocol: u8,
pub algorithm: u8,
pub public_key: Vec<u8>,
}
//TODO: validate values
impl FromBytes for DNSKeyRData {
fn from_bytes(reader: &mut Reader) -> Result<Self, ParseError> {
Ok(DNSKeyRData {
flags: reader.read_u16()?,
protocol: reader.read_u8()?,
algorithm: reader.read_u8()?,
public_key: reader.read(reader.unread_bytes())?,
})
}
}
impl DNSKeyRData {
pub fn verify(&self, sig: &Sig) -> bool {
let encoded = BASE64_STANDARD.encode(&self.public_key);
match self.algorithm {
15 => sig.verify_ed25519(&encoded),
_ => false,
}
}
}

View file

@ -10,6 +10,7 @@ use self::sig::Sig;
use super::ResponseHandler; use super::ResponseHandler;
mod authenticate; mod authenticate;
mod dnskey;
mod sig; mod sig;
pub(super) struct UpdateHandler {} pub(super) struct UpdateHandler {}

View file

@ -12,10 +12,6 @@ pub(super) struct Sig {
key_rdata: KeyRData, key_rdata: KeyRData,
} }
pub(super) enum PublicKey {
ED25519(String),
}
impl Sig { impl Sig {
pub fn new(rr: &RR, datagram: &[u8]) -> Result<Sig, ParseError> { pub fn new(rr: &RR, datagram: &[u8]) -> Result<Sig, ParseError> {
let mut request = datagram[0..datagram.len() - 11 - rr.rdlength as usize].to_vec(); let mut request = datagram[0..datagram.len() - 11 - rr.rdlength as usize].to_vec();
@ -33,7 +29,16 @@ impl Sig {
}) })
} }
fn verify_ed25519(&self, key: String) -> bool { pub fn verify_ed25519(&self, key: &str) -> bool {
let blob = BASE64_STANDARD.decode(key).unwrap();
let pkey = ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, &blob);
pkey.verify(&self.raw_data, &self.key_rdata.signature)
.is_ok()
}
pub fn verify_ssh_ed25519(&self, key: &str) -> bool {
let blob = BASE64_STANDARD.decode(key).unwrap(); let blob = BASE64_STANDARD.decode(key).unwrap();
let pkey = ring::signature::UnparsedPublicKey::new( let pkey = ring::signature::UnparsedPublicKey::new(
@ -44,10 +49,4 @@ impl Sig {
pkey.verify(&self.raw_data, &self.key_rdata.signature) pkey.verify(&self.raw_data, &self.key_rdata.signature)
.is_ok() .is_ok()
} }
pub fn verify(&self, key: PublicKey) -> bool {
match key {
PublicKey::ED25519(pkey) => self.verify_ed25519(pkey),
}
}
} }

View file

@ -4,8 +4,7 @@ use crate::{
errors::ParseError, errors::ParseError,
reader::Reader, reader::Reader,
structs::{ structs::{
Class, DNSKeyRData, Header, KeyRData, LabelString, Message, Opcode, Question, RRClass, Class, Header, KeyRData, LabelString, Message, Opcode, Question, RRClass, RRType, Type, RR,
RRType, Type, RR,
}, },
}; };
@ -43,12 +42,14 @@ impl From<Class> for u16 {
impl From<u16> for Type { impl From<u16> for Type {
fn from(value: u16) -> Self { fn from(value: u16) -> Self {
//TODO: use macro
match value { match value {
x if x == RRType::A as u16 => Type::Type(RRType::A), x if x == RRType::A as u16 => Type::Type(RRType::A),
x if x == RRType::OPT as u16 => Type::Type(RRType::OPT), x if x == RRType::OPT as u16 => Type::Type(RRType::OPT),
x if x == RRType::SOA as u16 => Type::Type(RRType::SOA), x if x == RRType::SOA as u16 => Type::Type(RRType::SOA),
x if x == RRType::ANY as u16 => Type::Type(RRType::SOA), x if x == RRType::ANY as u16 => Type::Type(RRType::SOA),
x if x == RRType::KEY as u16 => Type::Type(RRType::KEY), x if x == RRType::KEY as u16 => Type::Type(RRType::KEY),
x if x == RRType::DNSKEY as u16 => Type::Type(RRType::DNSKEY),
x => Type::Other(x), x => Type::Other(x),
} }
} }
@ -327,21 +328,3 @@ impl FromBytes for KeyRData {
} }
} }
} }
impl FromBytes for DNSKeyRData {
fn from_bytes(reader: &mut Reader) -> Result<Self> {
if reader.unread_bytes() < 18 {
Err(ParseError {
object: String::from("DNSKeyRData"),
message: String::from("invalid rdata"),
})
} else {
Ok(DNSKeyRData {
flags: reader.read_u16()?,
protocol: reader.read_u8()?,
algorithm: reader.read_u8()?,
public_key: reader.read(reader.unread_bytes())?,
})
}
}
}

View file

@ -9,7 +9,8 @@ pub enum Type {
pub enum RRType { pub enum RRType {
A = 1, A = 1,
SOA = 6, SOA = 6,
KEY = 24, KEY = 24, //TODO: change to SIG
DNSKEY = 48,
OPT = 41, OPT = 41,
ANY = 255, ANY = 255,
} }
@ -106,12 +107,3 @@ pub struct KeyRData {
pub signer: LabelString, pub signer: LabelString,
pub signature: Vec<u8>, pub signature: Vec<u8>,
} }
/// https://datatracker.ietf.org/doc/html/rfc4034#section-2
#[derive(Debug)]
pub struct DNSKeyRData {
pub flags: u16,
pub protocol: u8,
pub algorithm: u8,
pub public_key: Vec<u8>,
}

View file

@ -1,4 +1,3 @@
pub fn vec_equal<T: PartialEq>(vec1: &[T], vec2: &[T]) -> bool { pub fn vec_equal<T: PartialEq>(vec1: &[T], vec2: &[T]) -> bool {
if vec1.len() != vec2.len() { if vec1.len() != vec2.len() {
return false; return false;