use std::net::Ipv4Addr;
use hmac::{Hmac, Mac};
use md5::{Digest, Md5};
use sha1::Sha1;
use sha2::Sha256;
use subtle::ConstantTimeEq;
use crate::field::Field;
use super::constants::{RIP_AFI_AUTH, RIP_ENTRY_LEN, RIP_HEADER_LEN};
use super::entry::RipEntry;
use super::registry::{
is_rip_auth_marker, rip_auth_type, rip_auth_type_code, RipAuthType, RIP_AUTH_TYPE_KEYED_DIGEST,
RIP_AUTH_TYPE_SIMPLE,
};
pub const RIP_SIMPLE_PASSWORD_LEN: usize = 16;
pub const RIP_AUTH_TRAILER_MARKER: u16 = 0x0001;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RipDigestAlgorithm {
#[default]
KeyedMd5,
HmacSha1,
HmacSha256,
}
impl RipDigestAlgorithm {
pub fn digest_len(self) -> usize {
match self {
RipDigestAlgorithm::KeyedMd5 => 16,
RipDigestAlgorithm::HmacSha1 => 20,
RipDigestAlgorithm::HmacSha256 => 32,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RipAuth {
pub auth_type: Field<u16>,
pub payload: RipAuthPayload,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RipAuthPayload {
SimplePassword([u8; RIP_SIMPLE_PASSWORD_LEN]),
KeyedDigest(RipKeyedDigestHeader),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RipKeyedDigestHeader {
pub algorithm: RipDigestAlgorithm,
pub offset: Field<u16>,
pub key_id: Field<u8>,
pub auth_data_len: Field<u8>,
pub sequence: Field<u32>,
pub reserved1: Field<u32>,
pub reserved2: Field<u32>,
pub digest: Option<[u8; 16]>,
}
impl RipKeyedDigestHeader {
pub fn new() -> Self {
Self {
algorithm: RipDigestAlgorithm::default(),
offset: Field::defaulted(0),
key_id: Field::defaulted(0),
auth_data_len: Field::defaulted(0),
sequence: Field::defaulted(0),
reserved1: Field::defaulted(0),
reserved2: Field::defaulted(0),
digest: None,
}
}
pub fn offset_value(&self) -> u16 {
self.offset.value().copied().unwrap_or(0)
}
pub fn key_id_value(&self) -> u8 {
self.key_id.value().copied().unwrap_or(0)
}
pub fn auth_data_len_value(&self) -> u8 {
if self.auth_data_len.is_user_set() {
self.auth_data_len.value().copied().unwrap_or(0)
} else {
self.algorithm.digest_len() as u8
}
}
pub fn sequence_value(&self) -> u32 {
self.sequence.value().copied().unwrap_or(0)
}
}
impl Default for RipKeyedDigestHeader {
fn default() -> Self {
Self::new()
}
}
impl RipAuth {
pub fn simple_password(password: &[u8]) -> Self {
let mut bytes = [0u8; RIP_SIMPLE_PASSWORD_LEN];
let take = password.len().min(RIP_SIMPLE_PASSWORD_LEN);
bytes[..take].copy_from_slice(&password[..take]);
Self {
auth_type: Field::user(RIP_AUTH_TYPE_SIMPLE),
payload: RipAuthPayload::SimplePassword(bytes),
}
}
pub fn keyed_digest(key_id: u8, auth_data_len: u8) -> Self {
let mut header = RipKeyedDigestHeader::new();
header.key_id.set_user(key_id);
header.auth_data_len.set_user(auth_data_len);
Self {
auth_type: Field::user(RIP_AUTH_TYPE_KEYED_DIGEST),
payload: RipAuthPayload::KeyedDigest(header),
}
}
pub fn keyed_digest_with(alg: RipDigestAlgorithm, key_id: u8) -> Self {
let mut header = RipKeyedDigestHeader::new();
header.algorithm = alg;
header.key_id.set_user(key_id);
Self {
auth_type: Field::user(RIP_AUTH_TYPE_KEYED_DIGEST),
payload: RipAuthPayload::KeyedDigest(header),
}
}
pub fn auth_type_value(&self) -> u16 {
self.auth_type
.value()
.copied()
.unwrap_or(RIP_AUTH_TYPE_SIMPLE)
}
pub fn auth_type(&self) -> RipAuthType {
rip_auth_type(self.auth_type_value())
}
pub fn auth_data_len(&self) -> u8 {
match &self.payload {
RipAuthPayload::KeyedDigest(header) => header.auth_data_len_value(),
RipAuthPayload::SimplePassword(_) => 0,
}
}
pub fn keyed_digest_header_entry(&self) -> RipEntry {
let header = match &self.payload {
RipAuthPayload::KeyedDigest(header) => header,
RipAuthPayload::SimplePassword(_) => return self.as_entry(),
};
let offset = header.offset_value().to_be_bytes();
let sequence = header.sequence_value();
let address = Ipv4Addr::new(
offset[0],
offset[1],
header.key_id_value(),
header.auth_data_len_value(),
);
RipEntry::new()
.address_family(RIP_AFI_AUTH)
.route_tag(RIP_AUTH_TYPE_KEYED_DIGEST)
.address(address)
.subnet_mask(Ipv4Addr::from(sequence))
.next_hop(Ipv4Addr::UNSPECIFIED)
.metric(0)
}
pub fn trailing_digest_block(digest: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + digest.len());
out.extend_from_slice(&RIP_AFI_AUTH.to_be_bytes());
out.extend_from_slice(&RIP_AUTH_TRAILER_MARKER.to_be_bytes());
out.extend_from_slice(digest);
out
}
pub fn as_entry(&self) -> RipEntry {
let entry = RipEntry::new()
.address_family(RIP_AFI_AUTH)
.route_tag(rip_auth_type_code(self.auth_type()));
match &self.payload {
RipAuthPayload::SimplePassword(password) => {
let address = Ipv4Addr::new(password[0], password[1], password[2], password[3]);
let subnet_mask = Ipv4Addr::new(password[4], password[5], password[6], password[7]);
let next_hop = Ipv4Addr::new(password[8], password[9], password[10], password[11]);
let metric =
u32::from_be_bytes([password[12], password[13], password[14], password[15]]);
entry
.address(address)
.subnet_mask(subnet_mask)
.next_hop(next_hop)
.metric(metric)
}
RipAuthPayload::KeyedDigest(_) => entry,
}
}
}
pub fn decode_auth_entry(entry: &RipEntry) -> Option<RipAuth> {
if !is_rip_auth_marker(entry.address_family_value()) {
return None;
}
if entry.route_tag_value() != RIP_AUTH_TYPE_SIMPLE {
return None;
}
let mut password = [0u8; RIP_SIMPLE_PASSWORD_LEN];
password[0..4].copy_from_slice(&entry.address_value().octets());
password[4..8].copy_from_slice(&entry.subnet_mask_value().octets());
password[8..12].copy_from_slice(&entry.next_hop_value().octets());
password[12..16].copy_from_slice(&entry.metric_value().to_be_bytes());
Some(RipAuth::simple_password(&password))
}
pub const RIP_MD5_DIGEST_LEN: usize = 16;
pub(crate) fn compute_md5_digest(
message_with_key_region: &[u8],
key: &[u8],
) -> [u8; RIP_MD5_DIGEST_LEN] {
let mut buf = message_with_key_region.to_vec();
let region_start = buf.len().saturating_sub(RIP_MD5_DIGEST_LEN);
let region = &mut buf[region_start..];
let key_take = key.len().min(region.len());
for byte in region.iter_mut() {
*byte = 0;
}
region[..key_take].copy_from_slice(&key[..key_take]);
let mut hasher = Md5::new();
hasher.update(&buf);
let out = hasher.finalize();
let mut digest = [0u8; RIP_MD5_DIGEST_LEN];
digest.copy_from_slice(&out);
digest
}
pub(crate) fn compute_hmac_digest(alg: RipDigestAlgorithm, message: &[u8], key: &[u8]) -> Vec<u8> {
match alg {
RipDigestAlgorithm::KeyedMd5 => compute_md5_digest(message, key).to_vec(),
RipDigestAlgorithm::HmacSha1 => {
let mut mac = Hmac::<Sha1>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(message);
mac.finalize().into_bytes().to_vec()
}
RipDigestAlgorithm::HmacSha256 => {
let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(message);
mac.finalize().into_bytes().to_vec()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RipAuthVerification {
Unauthenticated,
SimplePasswordOk,
SimplePasswordMismatch,
DigestOk,
DigestMismatch,
}
pub fn verify(message_bytes: &[u8], key: &[u8]) -> RipAuthVerification {
if message_bytes.len() < RIP_HEADER_LEN + RIP_ENTRY_LEN {
return RipAuthVerification::Unauthenticated;
}
let entry = &message_bytes[RIP_HEADER_LEN..RIP_HEADER_LEN + RIP_ENTRY_LEN];
let afi = u16::from_be_bytes([entry[0], entry[1]]);
if !is_rip_auth_marker(afi) {
return RipAuthVerification::Unauthenticated;
}
let auth_type = u16::from_be_bytes([entry[2], entry[3]]);
match rip_auth_type(auth_type) {
RipAuthType::SimplePassword => {
let mut expected = [0u8; RIP_SIMPLE_PASSWORD_LEN];
let take = key.len().min(RIP_SIMPLE_PASSWORD_LEN);
expected[..take].copy_from_slice(&key[..take]);
let matches: bool = entry[4..RIP_ENTRY_LEN].ct_eq(&expected).into();
if matches {
RipAuthVerification::SimplePasswordOk
} else {
RipAuthVerification::SimplePasswordMismatch
}
}
RipAuthType::KeyedMessageDigest => {
let auth_data_len = entry[7] as usize;
let alg = match auth_data_len {
20 => RipDigestAlgorithm::HmacSha1,
32 => RipDigestAlgorithm::HmacSha256,
_ => RipDigestAlgorithm::KeyedMd5,
};
if message_bytes.len() < auth_data_len {
return RipAuthVerification::DigestMismatch;
}
let digest_start = message_bytes.len() - auth_data_len;
let transmitted = &message_bytes[digest_start..];
let recomputed = match alg {
RipDigestAlgorithm::KeyedMd5 => compute_md5_digest(message_bytes, key).to_vec(),
RipDigestAlgorithm::HmacSha1 | RipDigestAlgorithm::HmacSha256 => {
compute_hmac_digest(alg, &message_bytes[..digest_start], key)
}
};
let matches: bool = recomputed.as_slice().ct_eq(transmitted).into();
if matches {
RipAuthVerification::DigestOk
} else {
RipAuthVerification::DigestMismatch
}
}
RipAuthType::Other(_) => RipAuthVerification::DigestMismatch,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rip_auth_builders_set_type() {
let simple = RipAuth::simple_password(b"secret");
assert_eq!(simple.auth_type_value(), RIP_AUTH_TYPE_SIMPLE);
assert_eq!(simple.auth_type_value(), 2);
assert_eq!(simple.auth_type(), RipAuthType::SimplePassword);
assert!(matches!(simple.payload, RipAuthPayload::SimplePassword(_)));
let keyed = RipAuth::keyed_digest(1, 16);
assert_eq!(keyed.auth_type_value(), RIP_AUTH_TYPE_KEYED_DIGEST);
assert_eq!(keyed.auth_type_value(), 3);
assert_eq!(keyed.auth_type(), RipAuthType::KeyedMessageDigest);
match &keyed.payload {
RipAuthPayload::KeyedDigest(header) => {
assert_eq!(header.key_id_value(), 1);
assert_eq!(header.auth_data_len_value(), 16);
}
other => panic!("expected KeyedDigest payload, got {other:?}"),
}
let simple_entry = simple.as_entry();
assert_eq!(simple_entry.address_family_value(), RIP_AFI_AUTH);
assert_eq!(simple_entry.address_family_value(), 0xFFFF);
assert_eq!(simple_entry.route_tag_value(), RIP_AUTH_TYPE_SIMPLE);
assert!(simple_entry.is_auth_marker());
let keyed_entry = keyed.as_entry();
assert_eq!(keyed_entry.address_family_value(), RIP_AFI_AUTH);
assert_eq!(keyed_entry.route_tag_value(), RIP_AUTH_TYPE_KEYED_DIGEST);
assert!(keyed_entry.is_auth_marker());
}
#[test]
fn simple_password_pads_and_truncates_to_16_octets() {
let short = RipAuth::simple_password(b"pw");
match short.payload {
RipAuthPayload::SimplePassword(bytes) => {
assert_eq!(&bytes[..2], b"pw");
assert!(bytes[2..].iter().all(|&b| b == 0));
}
other => panic!("expected SimplePassword payload, got {other:?}"),
}
let long = RipAuth::simple_password(b"0123456789ABCDEF_overflow");
match long.payload {
RipAuthPayload::SimplePassword(bytes) => {
assert_eq!(&bytes, b"0123456789ABCDEF");
}
other => panic!("expected SimplePassword payload, got {other:?}"),
}
}
#[test]
fn rip_auth_simple_password_roundtrips() {
let auth = RipAuth::simple_password(b"secret");
let entry = auth.as_entry();
let mut bytes = Vec::new();
entry.encode(&mut bytes);
assert_eq!(bytes.len(), 20);
assert_eq!(&bytes[0..2], &[0xFF, 0xFF]);
assert_eq!(&bytes[2..4], &[0x00, 0x02]);
let mut expected_password = [0u8; RIP_SIMPLE_PASSWORD_LEN];
expected_password[..b"secret".len()].copy_from_slice(b"secret");
assert_eq!(&bytes[4..20], &expected_password);
let recovered = decode_auth_entry(&entry).expect("simple-password auth entry decodes");
assert_eq!(recovered.auth_type_value(), RIP_AUTH_TYPE_SIMPLE);
match recovered.payload {
RipAuthPayload::SimplePassword(bytes) => {
assert_eq!(&bytes, &expected_password);
}
other => panic!("expected SimplePassword payload, got {other:?}"),
}
}
#[test]
fn rip_auth_keyed_digest_layout() {
let mut auth = RipAuth::keyed_digest(7, 16);
match &mut auth.payload {
RipAuthPayload::KeyedDigest(header) => {
header.sequence.set_user(42);
}
other => panic!("expected KeyedDigest payload, got {other:?}"),
}
assert_eq!(auth.auth_data_len(), 16);
let header_entry = auth.keyed_digest_header_entry();
let mut header_bytes = Vec::new();
header_entry.encode(&mut header_bytes);
assert_eq!(header_bytes.len(), RIP_ENTRY_LEN);
assert_eq!(&header_bytes[0..2], &[0xFF, 0xFF]);
assert_eq!(&header_bytes[2..4], &[0x00, 0x03]);
assert_eq!(&header_bytes[4..6], &[0x00, 0x00]);
assert_eq!(header_bytes[6], 0x07);
assert_eq!(header_bytes[7], 16);
assert_eq!(&header_bytes[8..12], &42u32.to_be_bytes());
assert!(header_bytes[12..20].iter().all(|&b| b == 0));
let digest = [0xAB_u8; 16];
let block = RipAuth::trailing_digest_block(&digest);
assert_eq!(block.len(), 4 + digest.len());
assert_eq!(&block[0..2], &[0xFF, 0xFF]);
assert_eq!(&block[2..4], &[0x00, 0x01]);
assert_eq!(&block[4..], &digest);
}
#[test]
fn rip_auth_md5_known_vector() {
let message_with_key_region: Vec<u8> = {
let mut m = vec![0x02, 0x02, 0x00, 0x00];
m.extend_from_slice(&[0u8; RIP_MD5_DIGEST_LEN]);
m
};
let key = b"rip-md5-key";
let expected: [u8; RIP_MD5_DIGEST_LEN] = [
0x81, 0xe1, 0xbb, 0x86, 0xc7, 0x27, 0x6e, 0x81, 0xfa, 0x30, 0xdf, 0x78, 0x48, 0xe0,
0x00, 0x97,
];
let digest = compute_md5_digest(&message_with_key_region, key);
assert_eq!(digest, expected);
assert_eq!(compute_md5_digest(&message_with_key_region, key), expected);
assert_ne!(
compute_md5_digest(&message_with_key_region, b"other-key"),
expected
);
}
#[test]
fn rip_auth_hmac_sha_lengths() {
let message = b"\x02\x02\x00\x00rip-hmac-sample-message";
let key = b"rip-hmac-key";
let sha1 = compute_hmac_digest(RipDigestAlgorithm::HmacSha1, message, key);
assert_eq!(sha1.len(), 20);
assert_eq!(sha1.len(), RipDigestAlgorithm::HmacSha1.digest_len());
let sha256 = compute_hmac_digest(RipDigestAlgorithm::HmacSha256, message, key);
assert_eq!(sha256.len(), 32);
assert_eq!(sha256.len(), RipDigestAlgorithm::HmacSha256.digest_len());
let md5 = compute_hmac_digest(RipDigestAlgorithm::KeyedMd5, message, key);
assert_eq!(md5.len(), 16);
assert_eq!(md5.len(), RipDigestAlgorithm::KeyedMd5.digest_len());
let auth = RipAuth::keyed_digest_with(RipDigestAlgorithm::HmacSha1, 5);
assert_eq!(auth.auth_data_len(), 20);
match &auth.payload {
RipAuthPayload::KeyedDigest(header) => {
assert_eq!(header.algorithm, RipDigestAlgorithm::HmacSha1);
assert_eq!(header.key_id_value(), 5);
}
other => panic!("expected KeyedDigest payload, got {other:?}"),
}
let auth256 = RipAuth::keyed_digest_with(RipDigestAlgorithm::HmacSha256, 9);
assert_eq!(auth256.auth_data_len(), 32);
let md5_auth = RipAuth::keyed_digest(1, 16);
assert_eq!(md5_auth.auth_data_len(), 16);
match &md5_auth.payload {
RipAuthPayload::KeyedDigest(header) => {
assert_eq!(header.algorithm, RipDigestAlgorithm::KeyedMd5);
}
other => panic!("expected KeyedDigest payload, got {other:?}"),
}
}
#[test]
fn rip_auth_hmac_sha1_known_vector() {
let message = b"\x02\x02\x00\x00rip-hmac-sha1-known-vector";
let key = b"rip-sha1-key";
let expected: [u8; 20] = [
0x05, 0x36, 0x0e, 0x18, 0xb1, 0x19, 0x7f, 0xbe, 0xa6, 0x00, 0x00, 0xd1, 0x66, 0x52,
0x6c, 0x8d, 0xd7, 0x77, 0x15, 0x8d,
];
let digest = compute_hmac_digest(RipDigestAlgorithm::HmacSha1, message, key);
assert_eq!(digest.len(), 20);
assert_eq!(digest.as_slice(), &expected);
assert_eq!(
compute_hmac_digest(RipDigestAlgorithm::HmacSha1, message, key),
digest
);
assert_ne!(
compute_hmac_digest(RipDigestAlgorithm::HmacSha1, message, b"other-key"),
digest
);
}
#[test]
fn rip_auth_verify_simple_password() {
let key = b"rip-pass";
let auth = RipAuth::simple_password(key);
let mut message = vec![
crate::protocols::rip::constants::RIP_COMMAND_RESPONSE,
crate::protocols::rip::constants::RIP_VERSION_2,
0x00,
0x00,
];
auth.as_entry().encode(&mut message);
assert_eq!(verify(&message, key), RipAuthVerification::SimplePasswordOk);
assert_eq!(
verify(&message, b"wrong-pass"),
RipAuthVerification::SimplePasswordMismatch
);
let mut unauthenticated = vec![
crate::protocols::rip::constants::RIP_COMMAND_RESPONSE,
crate::protocols::rip::constants::RIP_VERSION_2,
0x00,
0x00,
];
RipEntry::ipv2_route(
std::net::Ipv4Addr::new(192, 0, 2, 0),
std::net::Ipv4Addr::new(255, 255, 255, 0),
1,
)
.encode(&mut unauthenticated);
assert_eq!(
verify(&unauthenticated, key),
RipAuthVerification::Unauthenticated
);
}
#[test]
fn rip_auth_verify_keyed_md5() {
let key = b"rip-md5-key";
let auth = RipAuth::keyed_digest(1, RIP_MD5_DIGEST_LEN as u8);
let mut message: Vec<u8> = vec![
crate::protocols::rip::constants::RIP_COMMAND_RESPONSE,
crate::protocols::rip::constants::RIP_VERSION_2,
0x00,
0x00,
];
auth.keyed_digest_header_entry().encode(&mut message);
RipEntry::ipv2_route(
std::net::Ipv4Addr::new(192, 0, 2, 0),
std::net::Ipv4Addr::new(255, 255, 255, 0),
1,
)
.encode(&mut message);
message.extend_from_slice(&RIP_AFI_AUTH.to_be_bytes());
message.extend_from_slice(&RIP_AUTH_TRAILER_MARKER.to_be_bytes());
let mut message_with_key_region = message.clone();
message_with_key_region.extend_from_slice(&[0u8; RIP_MD5_DIGEST_LEN]);
let digest = compute_md5_digest(&message_with_key_region, key);
message.extend_from_slice(&digest);
assert_eq!(verify(&message, key), RipAuthVerification::DigestOk);
assert_eq!(
verify(&message, b"other-key"),
RipAuthVerification::DigestMismatch
);
}
}