1use crate::server::radius::packet::{
14 accounting_request_authenticator, encode_user_password, response_authenticator, Attribute,
15 RadiusPacket, ATTR_CHAP_PASSWORD, ATTR_MESSAGE_AUTHENTICATOR, ATTR_USER_PASSWORD,
16 CODE_ACCESS_ACCEPT, CODE_ACCESS_CHALLENGE, CODE_ACCESS_REJECT, CODE_ACCESS_REQUEST,
17 CODE_ACCOUNTING_REQUEST, CODE_ACCOUNTING_RESPONSE, CODE_STATUS_SERVER, HEADER_LEN,
18 MAX_PACKET_LEN,
19};
20use md5::{Digest, Md5};
21
22pub use crate::server::radius::packet::hmac_md5;
23
24pub fn chap_password(chap_id: u8, password: &[u8], challenge: &[u8; 16]) -> Vec<u8> {
29 let mut hasher = Md5::new();
30 hasher.update([chap_id]);
31 hasher.update(password);
32 hasher.update(challenge);
33 let mut out = vec![chap_id];
34 out.extend_from_slice(&hasher.finalize());
35 out
36}
37
38pub enum Credential<'a> {
40 Pap(&'a [u8]),
41 Chap(&'a [u8]),
42 None,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct BuildError(pub String);
49
50impl std::fmt::Display for BuildError {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.write_str(&self.0)
53 }
54}
55
56fn header(
57 code: u8,
58 identifier: u8,
59 authenticator: &[u8; 16],
60 attrs: &[u8],
61) -> Result<Vec<u8>, BuildError> {
62 let total = HEADER_LEN + attrs.len();
63 if total > MAX_PACKET_LEN {
64 return Err(BuildError(format!(
65 "the packet would be {total} bytes; RADIUS allows at most {MAX_PACKET_LEN}"
66 )));
67 }
68 let mut out = Vec::with_capacity(total);
69 out.push(code);
70 out.push(identifier);
71 out.extend_from_slice(&u16::try_from(total).unwrap_or(u16::MAX).to_be_bytes());
73 out.extend_from_slice(authenticator);
74 out.extend_from_slice(attrs);
75 Ok(out)
76}
77
78fn encode_all(attributes: &[Attribute]) -> Result<Vec<u8>, BuildError> {
79 let mut out = Vec::new();
80 for a in attributes {
81 out.extend_from_slice(&a.encode().map_err(|e| BuildError(e.to_string()))?);
82 }
83 Ok(out)
84}
85
86fn sign_with_message_authenticator(
90 mut packet: Vec<u8>,
91 secret: &[u8],
92) -> Result<Vec<u8>, BuildError> {
93 let at = packet.len();
94 packet.extend_from_slice(&[ATTR_MESSAGE_AUTHENTICATOR, 18]);
95 packet.extend_from_slice(&[0u8; 16]);
96 if packet.len() > MAX_PACKET_LEN {
97 return Err(BuildError(format!(
98 "the packet would be {} bytes; RADIUS allows at most {MAX_PACKET_LEN}",
99 packet.len()
100 )));
101 }
102 let len = u16::try_from(packet.len()).unwrap_or(u16::MAX);
103 packet[2..4].copy_from_slice(&len.to_be_bytes());
104 let mac = hmac_md5(secret, &packet);
105 packet[at + 2..at + 18].copy_from_slice(&mac);
106 Ok(packet)
107}
108
109pub fn access_request(
115 identifier: u8,
116 request_authenticator: &[u8; 16],
117 credential: Credential<'_>,
118 attributes: &[Attribute],
119 secret: &[u8],
120) -> Result<Vec<u8>, BuildError> {
121 let mut attrs = attributes.to_vec();
122 match credential {
123 Credential::Pap(password) => {
124 let hidden = encode_user_password(password, request_authenticator, secret)
125 .map_err(|e| BuildError(e.to_string()))?;
126 attrs.push(Attribute::new(ATTR_USER_PASSWORD, hidden));
127 }
128 Credential::Chap(password) => {
129 attrs.push(Attribute::new(
132 ATTR_CHAP_PASSWORD,
133 chap_password(identifier, password, request_authenticator),
134 ));
135 }
136 Credential::None => {}
137 }
138 let packet = header(
139 CODE_ACCESS_REQUEST,
140 identifier,
141 request_authenticator,
142 &encode_all(&attrs)?,
143 )?;
144 sign_with_message_authenticator(packet, secret)
145}
146
147pub fn status_server(
149 identifier: u8,
150 request_authenticator: &[u8; 16],
151 secret: &[u8],
152) -> Result<Vec<u8>, BuildError> {
153 let packet = header(CODE_STATUS_SERVER, identifier, request_authenticator, &[])?;
154 sign_with_message_authenticator(packet, secret)
155}
156
157pub fn accounting_request(
160 identifier: u8,
161 attributes: &[Attribute],
162 secret: &[u8],
163) -> Result<(Vec<u8>, [u8; 16]), BuildError> {
164 let attrs = encode_all(attributes)?;
165 let length = u16::try_from(HEADER_LEN + attrs.len())
166 .ok()
167 .filter(|l| usize::from(*l) <= MAX_PACKET_LEN)
168 .ok_or_else(|| BuildError("accounting attributes exceed 4096 bytes".to_string()))?;
169 let auth = accounting_request_authenticator(identifier, length, &attrs, secret);
170 Ok((
171 header(CODE_ACCOUNTING_REQUEST, identifier, &auth, &attrs)?,
172 auth,
173 ))
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub enum ReplyError {
179 Malformed(String),
181 UnexpectedCode(u8),
183 BadAuthenticator,
186 BadMessageAuthenticator,
188 MissingMessageAuthenticator,
192}
193
194impl ReplyError {
195 pub fn kind(&self) -> &'static str {
196 match self {
197 ReplyError::Malformed(_) => "malformed",
198 ReplyError::UnexpectedCode(_) => "unexpected_code",
199 ReplyError::BadAuthenticator => "bad_authenticator",
200 ReplyError::BadMessageAuthenticator => "bad_message_authenticator",
201 ReplyError::MissingMessageAuthenticator => "missing_message_authenticator",
202 }
203 }
204}
205
206impl std::fmt::Display for ReplyError {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 match self {
209 ReplyError::Malformed(e) => write!(f, "not a RADIUS packet: {e}"),
210 ReplyError::UnexpectedCode(c) => write!(
211 f,
212 "reply code {c} ({}) does not answer the request",
213 crate::server::radius::packet::code_name(*c)
214 ),
215 ReplyError::BadAuthenticator => write!(
216 f,
217 "the Response Authenticator does not verify: the sender does not hold the \
218 shared secret, or the packet was altered"
219 ),
220 ReplyError::BadMessageAuthenticator => {
221 write!(f, "the Message-Authenticator does not verify")
222 }
223 ReplyError::MissingMessageAuthenticator => write!(
224 f,
225 "the reply carries no Message-Authenticator, which this client requires on \
226 Access-Accept/Reject/Challenge and Status-Server answers"
227 ),
228 }
229 }
230}
231
232fn answers(request_code: u8, reply_code: u8) -> bool {
234 match request_code {
235 CODE_ACCESS_REQUEST => matches!(
236 reply_code,
237 CODE_ACCESS_ACCEPT | CODE_ACCESS_REJECT | CODE_ACCESS_CHALLENGE
238 ),
239 CODE_ACCOUNTING_REQUEST => reply_code == CODE_ACCOUNTING_RESPONSE,
240 CODE_STATUS_SERVER => matches!(reply_code, CODE_ACCESS_ACCEPT | CODE_ACCOUNTING_RESPONSE),
241 _ => false,
242 }
243}
244
245pub fn verify_reply(
250 datagram: &[u8],
251 request_code: u8,
252 request_authenticator: &[u8; 16],
253 secret: &[u8],
254) -> Result<RadiusPacket, ReplyError> {
255 let packet =
256 RadiusPacket::decode(datagram).map_err(|e| ReplyError::Malformed(e.to_string()))?;
257 if !answers(request_code, packet.code) {
258 return Err(ReplyError::UnexpectedCode(packet.code));
259 }
260 let declared = usize::from(u16::from_be_bytes([datagram[2], datagram[3]]));
262 let attrs = &datagram[HEADER_LEN..declared];
263 let expected = response_authenticator(
264 packet.code,
265 packet.identifier,
266 attrs,
267 request_authenticator,
268 secret,
269 );
270 if !constant_time_eq(&expected, &packet.authenticator) {
271 return Err(ReplyError::BadAuthenticator);
272 }
273
274 let mut offset = HEADER_LEN;
276 let mut ma_at = None;
277 while offset + 2 <= declared {
278 let len = usize::from(datagram[offset + 1]);
279 if datagram[offset] == ATTR_MESSAGE_AUTHENTICATOR && len == 18 {
280 ma_at = Some(offset);
281 }
282 if len < 2 {
283 break;
284 }
285 offset += len;
286 }
287 let requires_ma = request_code == CODE_STATUS_SERVER || request_code == CODE_ACCESS_REQUEST;
288 match ma_at {
289 None if requires_ma => Err(ReplyError::MissingMessageAuthenticator),
290 None => Ok(packet),
291 Some(at) => {
292 let mut copy = datagram[..declared].to_vec();
293 copy[4..20].copy_from_slice(request_authenticator);
294 copy[at + 2..at + 18].fill(0);
295 let mac = hmac_md5(secret, ©);
296 let mut got = [0u8; 16];
297 got.copy_from_slice(&datagram[at + 2..at + 18]);
298 if constant_time_eq(&mac, &got) {
299 Ok(packet)
300 } else {
301 Err(ReplyError::BadMessageAuthenticator)
302 }
303 }
304 }
305}
306
307fn constant_time_eq(a: &[u8; 16], b: &[u8; 16]) -> bool {
308 a.iter().zip(b).fold(0u8, |d, (x, y)| d | (x ^ y)) == 0
309}