use crate::server::radius::packet::{
accounting_request_authenticator, encode_user_password, response_authenticator, Attribute,
RadiusPacket, ATTR_CHAP_PASSWORD, ATTR_MESSAGE_AUTHENTICATOR, ATTR_USER_PASSWORD,
CODE_ACCESS_ACCEPT, CODE_ACCESS_CHALLENGE, CODE_ACCESS_REJECT, CODE_ACCESS_REQUEST,
CODE_ACCOUNTING_REQUEST, CODE_ACCOUNTING_RESPONSE, CODE_STATUS_SERVER, HEADER_LEN,
MAX_PACKET_LEN,
};
use md5::{Digest, Md5};
pub use crate::server::radius::packet::hmac_md5;
pub fn chap_password(chap_id: u8, password: &[u8], challenge: &[u8; 16]) -> Vec<u8> {
let mut hasher = Md5::new();
hasher.update([chap_id]);
hasher.update(password);
hasher.update(challenge);
let mut out = vec![chap_id];
out.extend_from_slice(&hasher.finalize());
out
}
pub enum Credential<'a> {
Pap(&'a [u8]),
Chap(&'a [u8]),
None,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BuildError(pub String);
impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
fn header(
code: u8,
identifier: u8,
authenticator: &[u8; 16],
attrs: &[u8],
) -> Result<Vec<u8>, BuildError> {
let total = HEADER_LEN + attrs.len();
if total > MAX_PACKET_LEN {
return Err(BuildError(format!(
"the packet would be {total} bytes; RADIUS allows at most {MAX_PACKET_LEN}"
)));
}
let mut out = Vec::with_capacity(total);
out.push(code);
out.push(identifier);
out.extend_from_slice(&u16::try_from(total).unwrap_or(u16::MAX).to_be_bytes());
out.extend_from_slice(authenticator);
out.extend_from_slice(attrs);
Ok(out)
}
fn encode_all(attributes: &[Attribute]) -> Result<Vec<u8>, BuildError> {
let mut out = Vec::new();
for a in attributes {
out.extend_from_slice(&a.encode().map_err(|e| BuildError(e.to_string()))?);
}
Ok(out)
}
fn sign_with_message_authenticator(
mut packet: Vec<u8>,
secret: &[u8],
) -> Result<Vec<u8>, BuildError> {
let at = packet.len();
packet.extend_from_slice(&[ATTR_MESSAGE_AUTHENTICATOR, 18]);
packet.extend_from_slice(&[0u8; 16]);
if packet.len() > MAX_PACKET_LEN {
return Err(BuildError(format!(
"the packet would be {} bytes; RADIUS allows at most {MAX_PACKET_LEN}",
packet.len()
)));
}
let len = u16::try_from(packet.len()).unwrap_or(u16::MAX);
packet[2..4].copy_from_slice(&len.to_be_bytes());
let mac = hmac_md5(secret, &packet);
packet[at + 2..at + 18].copy_from_slice(&mac);
Ok(packet)
}
pub fn access_request(
identifier: u8,
request_authenticator: &[u8; 16],
credential: Credential<'_>,
attributes: &[Attribute],
secret: &[u8],
) -> Result<Vec<u8>, BuildError> {
let mut attrs = attributes.to_vec();
match credential {
Credential::Pap(password) => {
let hidden = encode_user_password(password, request_authenticator, secret)
.map_err(|e| BuildError(e.to_string()))?;
attrs.push(Attribute::new(ATTR_USER_PASSWORD, hidden));
}
Credential::Chap(password) => {
attrs.push(Attribute::new(
ATTR_CHAP_PASSWORD,
chap_password(identifier, password, request_authenticator),
));
}
Credential::None => {}
}
let packet = header(
CODE_ACCESS_REQUEST,
identifier,
request_authenticator,
&encode_all(&attrs)?,
)?;
sign_with_message_authenticator(packet, secret)
}
pub fn status_server(
identifier: u8,
request_authenticator: &[u8; 16],
secret: &[u8],
) -> Result<Vec<u8>, BuildError> {
let packet = header(CODE_STATUS_SERVER, identifier, request_authenticator, &[])?;
sign_with_message_authenticator(packet, secret)
}
pub fn accounting_request(
identifier: u8,
attributes: &[Attribute],
secret: &[u8],
) -> Result<(Vec<u8>, [u8; 16]), BuildError> {
let attrs = encode_all(attributes)?;
let length = u16::try_from(HEADER_LEN + attrs.len())
.ok()
.filter(|l| usize::from(*l) <= MAX_PACKET_LEN)
.ok_or_else(|| BuildError("accounting attributes exceed 4096 bytes".to_string()))?;
let auth = accounting_request_authenticator(identifier, length, &attrs, secret);
Ok((
header(CODE_ACCOUNTING_REQUEST, identifier, &auth, &attrs)?,
auth,
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReplyError {
Malformed(String),
UnexpectedCode(u8),
BadAuthenticator,
BadMessageAuthenticator,
MissingMessageAuthenticator,
}
impl ReplyError {
pub fn kind(&self) -> &'static str {
match self {
ReplyError::Malformed(_) => "malformed",
ReplyError::UnexpectedCode(_) => "unexpected_code",
ReplyError::BadAuthenticator => "bad_authenticator",
ReplyError::BadMessageAuthenticator => "bad_message_authenticator",
ReplyError::MissingMessageAuthenticator => "missing_message_authenticator",
}
}
}
impl std::fmt::Display for ReplyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReplyError::Malformed(e) => write!(f, "not a RADIUS packet: {e}"),
ReplyError::UnexpectedCode(c) => write!(
f,
"reply code {c} ({}) does not answer the request",
crate::server::radius::packet::code_name(*c)
),
ReplyError::BadAuthenticator => write!(
f,
"the Response Authenticator does not verify: the sender does not hold the \
shared secret, or the packet was altered"
),
ReplyError::BadMessageAuthenticator => {
write!(f, "the Message-Authenticator does not verify")
}
ReplyError::MissingMessageAuthenticator => write!(
f,
"the reply carries no Message-Authenticator, which this client requires on \
Access-Accept/Reject/Challenge and Status-Server answers"
),
}
}
}
fn answers(request_code: u8, reply_code: u8) -> bool {
match request_code {
CODE_ACCESS_REQUEST => matches!(
reply_code,
CODE_ACCESS_ACCEPT | CODE_ACCESS_REJECT | CODE_ACCESS_CHALLENGE
),
CODE_ACCOUNTING_REQUEST => reply_code == CODE_ACCOUNTING_RESPONSE,
CODE_STATUS_SERVER => matches!(reply_code, CODE_ACCESS_ACCEPT | CODE_ACCOUNTING_RESPONSE),
_ => false,
}
}
pub fn verify_reply(
datagram: &[u8],
request_code: u8,
request_authenticator: &[u8; 16],
secret: &[u8],
) -> Result<RadiusPacket, ReplyError> {
let packet =
RadiusPacket::decode(datagram).map_err(|e| ReplyError::Malformed(e.to_string()))?;
if !answers(request_code, packet.code) {
return Err(ReplyError::UnexpectedCode(packet.code));
}
let declared = usize::from(u16::from_be_bytes([datagram[2], datagram[3]]));
let attrs = &datagram[HEADER_LEN..declared];
let expected = response_authenticator(
packet.code,
packet.identifier,
attrs,
request_authenticator,
secret,
);
if !constant_time_eq(&expected, &packet.authenticator) {
return Err(ReplyError::BadAuthenticator);
}
let mut offset = HEADER_LEN;
let mut ma_at = None;
while offset + 2 <= declared {
let len = usize::from(datagram[offset + 1]);
if datagram[offset] == ATTR_MESSAGE_AUTHENTICATOR && len == 18 {
ma_at = Some(offset);
}
if len < 2 {
break;
}
offset += len;
}
let requires_ma = request_code == CODE_STATUS_SERVER || request_code == CODE_ACCESS_REQUEST;
match ma_at {
None if requires_ma => Err(ReplyError::MissingMessageAuthenticator),
None => Ok(packet),
Some(at) => {
let mut copy = datagram[..declared].to_vec();
copy[4..20].copy_from_slice(request_authenticator);
copy[at + 2..at + 18].fill(0);
let mac = hmac_md5(secret, ©);
let mut got = [0u8; 16];
got.copy_from_slice(&datagram[at + 2..at + 18]);
if constant_time_eq(&mac, &got) {
Ok(packet)
} else {
Err(ReplyError::BadMessageAuthenticator)
}
}
}
}
fn constant_time_eq(a: &[u8; 16], b: &[u8; 16]) -> bool {
a.iter().zip(b).fold(0u8, |d, (x, y)| d | (x ^ y)) == 0
}