Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add handling of NTPv5 to the server #1153

Merged
merged 8 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions ntp-proto/src/packet/extension_fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,12 @@ impl<'a> ExtensionFieldData<'a> {
let mut it = self.untrusted.iter().peekable();
while let Some(field) = it.next() {
let is_last = it.peek().is_none();
let minimum_size = if is_last { 28 } else { 16 };
let minimum_size = match version {
ExtensionHeaderVersion::V4 if is_last => 28,
ExtensionHeaderVersion::V4 => 16,
#[cfg(feature = "ntpv5")]
ExtensionHeaderVersion::V5 => 4,
};
field.serialize(w, minimum_size, version)?;
}

Expand All @@ -554,10 +559,15 @@ impl<'a> ExtensionFieldData<'a> {
let mut size = 0;
let mut is_valid_nts = true;
let mut cookie = None;
let mac_size = match version {
ExtensionHeaderVersion::V4 => Mac::MAXIMUM_SIZE,
#[cfg(feature = "ntpv5")]
ExtensionHeaderVersion::V5 => 0,
};

for field in RawExtensionField::deserialize_sequence(
&data[header_size..],
Mac::MAXIMUM_SIZE,
mac_size,
RawExtensionField::V4_UNENCRYPTED_MINIMUM_SIZE,
version,
) {
Expand Down Expand Up @@ -776,6 +786,10 @@ impl<'a> RawExtensionField<'a> {
return Err(IncorrectLength);
}

// In NTPv5: There must still be enough room in the packet for data + padding
data.get(4..next_multiple_of_usize(field_length, 4))
.ok_or(IncorrectLength)?;

// because the field length includes padding, the message bytes may not exactly match the input
let message_bytes = data.get(4..field_length).ok_or(IncorrectLength)?;

Expand Down
168 changes: 145 additions & 23 deletions ntp-proto/src/packet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ impl<'a> NtpPacket<'a> {
};

// TODO: Check extension field handling in V5
match ExtensionFieldData::deserialize(
let res_packet = match ExtensionFieldData::deserialize(
data,
header_size,
cipher,
Expand All @@ -407,6 +407,20 @@ impl<'a> NtpPacket<'a> {

Err(ParsingError::DecryptError(packet))
}
};

let (packet, cookie) = res_packet?;

match packet.draft_id() {
Some(id) if id == v5::DRAFT_VERSION => Ok((packet, cookie)),
received @ (Some(_) | None) => {
tracing::error!(
expected = v5::DRAFT_VERSION,
received,
"Mismatched draft ID ignoring packet!"
);
Err(ParsingError::V5(v5::V5Error::InvalidDraftIdentification))
}
}
}
_ => Err(PacketParsingError::InvalidVersion(version)),
Expand Down Expand Up @@ -505,6 +519,27 @@ impl<'a> NtpPacket<'a> {
)
}

#[cfg(feature = "ntpv5")]
pub fn poll_message_v5(poll_interval: PollInterval) -> (Self, RequestIdentifier) {
let (header, id) = v5::NtpHeaderV5::poll_message(poll_interval);

let draft_id = ExtensionField::DraftIdentification(Cow::Borrowed(v5::DRAFT_VERSION));

(
NtpPacket {
header: NtpHeader::V5(header),
efdata: ExtensionFieldData {
authenticated: vec![],
encrypted: vec![],
untrusted: vec![draft_id],
},
mac: None,
},
id,
)
}

#[cfg_attr(not(feature = "ntpv5"), allow(unused_mut))]
pub fn timestamp_response<C: NtpClock>(
system: &SystemSnapshot,
input: Self,
Expand All @@ -522,27 +557,38 @@ impl<'a> NtpPacket<'a> {
efdata: Default::default(),
mac: None,
},
NtpHeader::V4(header) => NtpPacket {
header: NtpHeader::V4(NtpHeaderV3V4::timestamp_response(
system,
header,
recv_timestamp,
clock,
)),
efdata: ExtensionFieldData {
authenticated: vec![],
encrypted: vec![],
// Ignore encrypted so as not to accidentaly leak anything
untrusted: input
.efdata
.untrusted
.into_iter()
.chain(input.efdata.authenticated)
.filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_)))
.collect(),
},
mac: None,
},
NtpHeader::V4(header) => {
let mut response_header =
NtpHeaderV3V4::timestamp_response(system, header, recv_timestamp, clock);

#[cfg(feature = "ntpv5")]
{
// Respond with the upgrade timestamp (NTP5NTP5) iff the input had it and the packet
// had the correct draft identification
if let (v5::UPGRADE_TIMESTAMP, Some(v5::DRAFT_VERSION)) =
(header.reference_timestamp, input.draft_id())
{
response_header.reference_timestamp = v5::UPGRADE_TIMESTAMP;
};
}

NtpPacket {
header: NtpHeader::V4(response_header),
efdata: ExtensionFieldData {
authenticated: vec![],
encrypted: vec![],
// Ignore encrypted so as not to accidentally leak anything
untrusted: input
.efdata
.untrusted
.into_iter()
.chain(input.efdata.authenticated)
.filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_)))
.collect(),
},
mac: None,
}
}
#[cfg(feature = "ntpv5")]
NtpHeader::V5(header) => NtpPacket {
// TODO deduplicate extension handling with V4
Expand All @@ -555,20 +601,31 @@ impl<'a> NtpPacket<'a> {
efdata: ExtensionFieldData {
authenticated: vec![],
encrypted: vec![],
// Ignore encrypted so as not to accidentaly leak anything
// Ignore encrypted so as not to accidentally leak anything
untrusted: input
.efdata
.untrusted
.into_iter()
.chain(input.efdata.authenticated)
.filter(|ef| matches!(ef, ExtensionField::UniqueIdentifier(_)))
.chain(std::iter::once(ExtensionField::DraftIdentification(
Cow::Borrowed(v5::DRAFT_VERSION),
)))
.collect(),
},
mac: None,
},
}
}

#[cfg(feature = "ntpv5")]
fn draft_id(&self) -> Option<&'_ str> {
self.efdata.untrusted.iter().find_map(|ef| match ef {
ExtensionField::DraftIdentification(id) => Some(&**id),
_ => None,
})
}

pub fn nts_timestamp_response<C: NtpClock>(
system: &SystemSnapshot,
input: Self,
Expand Down Expand Up @@ -760,6 +817,15 @@ impl<'a> NtpPacket<'a> {
})
}

pub fn version(&self) -> u8 {
match self.header {
NtpHeader::V3(_) => 3,
NtpHeader::V4(_) => 4,
#[cfg(feature = "ntpv5")]
NtpHeader::V5(_) => 5,
}
}

pub fn leap(&self) -> NtpLeapIndicator {
match self.header {
NtpHeader::V3(header) => header.leap,
Expand Down Expand Up @@ -1442,6 +1508,44 @@ mod tests {
assert!(!response.valid_server_response(id, true));
}

#[test]
fn v5_upgrade_packet() {
let (mut packet, _) = NtpPacket::poll_message(PollInterval::default());
let NtpHeader::V4(header) = &mut packet.header else {
panic!("wrong version");
};
header.reference_timestamp = NtpTimestamp::from_fixed_int(0x4E5450354E545035);

#[cfg(feature = "ntpv5")]
packet
.efdata
.untrusted
.push(ExtensionField::DraftIdentification(Cow::Borrowed(
v5::DRAFT_VERSION,
)));

let response = NtpPacket::timestamp_response(
&SystemSnapshot::default(),
packet,
NtpTimestamp::from_fixed_int(0),
&TestClock {
now: NtpTimestamp::from_fixed_int(1),
},
);

let NtpHeader::V4(header) = response.header else {
panic!("wrong version");
};

let expect = if cfg!(feature = "ntpv5") {
NtpTimestamp::from_fixed_int(0x4E5450354E545035)
} else {
NtpTimestamp::from_fixed_int(0)
};

assert_eq!(header.reference_timestamp, expect);
}

#[test]
fn test_timestamp_response() {
let decoded = DecodedServerCookie {
Expand Down Expand Up @@ -2029,4 +2133,22 @@ mod tests {
];
assert!(NtpPacket::deserialize(&input, &NoCipher).is_err());
}

#[cfg(feature = "ntpv5")]
#[test]
fn ef_with_missing_padding_v5() {
let (packet, _) = NtpPacket::poll_message_v5(PollInterval::default());
let mut data = packet.serialize_without_encryption_vec().unwrap();
data.extend([
0, 0, // Type = Unknown
0, 6, // Length = 5
1, 2, // Data
// Missing 2 padding bytes
]);

assert!(matches!(
NtpPacket::deserialize(&data, &NoCipher),
Err(ParsingError::IncorrectLength)
));
}
}
57 changes: 53 additions & 4 deletions ntp-proto/src/packet/v5/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![warn(clippy::missing_const_for_fn)]
use crate::{NtpClock, NtpDuration, NtpLeapIndicator, NtpTimestamp, SystemSnapshot};
use crate::{NtpClock, NtpDuration, NtpLeapIndicator, NtpTimestamp, PollInterval, SystemSnapshot};
use rand::random;

mod error;
Expand All @@ -9,8 +9,11 @@ pub mod extension_fields;
use crate::packet::error::ParsingError;
pub use error::V5Error;

use super::RequestIdentifier;

#[allow(dead_code)]
pub(crate) const DRAFT_VERSION: &str = "draft-ietf-ntp-ntpv5-00";
pub(crate) const UPGRADE_TIMESTAMP: NtpTimestamp = NtpTimestamp::from_bits(*b"NTP5NTP5");

#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
Expand Down Expand Up @@ -108,7 +111,7 @@ impl NtpFlags {
pub struct NtpServerCookie(pub [u8; 8]);

impl NtpServerCookie {
fn new_random() -> NtpServerCookie {
fn new_random() -> Self {
// TODO does this match entropy handling of the rest of the system?
Self(random())
}
Expand All @@ -118,9 +121,18 @@ impl NtpServerCookie {
pub struct NtpClientCookie(pub [u8; 8]);

impl NtpClientCookie {
fn new_random() -> Self {
// TODO does this match entropy handling of the rest of the system?
Self(random())
Comment on lines +125 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is kind of unfortunate for testing (potentially for reproducing fuzzing errors too)

}

pub const fn from_ntp_timestamp(ts: NtpTimestamp) -> Self {
Self(ts.to_bits())
}

pub const fn into_ntp_timestamp(self) -> NtpTimestamp {
NtpTimestamp::from_bits(self.0)
}
}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
Expand All @@ -144,6 +156,28 @@ pub struct NtpHeaderV5 {
}

impl NtpHeaderV5 {
fn new() -> Self {
Self {
leap: NtpLeapIndicator::NoWarning,
mode: NtpMode::Request,
stratum: 0,
poll: 0,
precision: 0,
root_delay: NtpDuration::default(),
root_dispersion: NtpDuration::default(),
receive_timestamp: NtpTimestamp::default(),
transmit_timestamp: NtpTimestamp::default(),
timescale: NtpTimescale::Utc,
era: NtpEra(0),
flags: NtpFlags {
unknown_leap: false,
interleaved_mode: false,
},
server_cookie: NtpServerCookie([0; 8]),
client_cookie: NtpClientCookie([0; 8]),
}
}

pub(crate) fn timestamp_response<C: NtpClock>(
system: &SystemSnapshot,
input: Self,
Expand Down Expand Up @@ -174,9 +208,7 @@ impl NtpHeaderV5 {
transmit_timestamp: clock.now().expect("Failed to read time"),
}
}
}

impl NtpHeaderV5 {
const WIRE_LENGTH: usize = 48;
const VERSION: u8 = 5;

Expand Down Expand Up @@ -228,6 +260,23 @@ impl NtpHeaderV5 {
w.write_all(&self.transmit_timestamp.to_bits())?;
Ok(())
}

pub fn poll_message(poll_interval: PollInterval) -> (Self, RequestIdentifier) {
let mut packet = Self::new();
packet.poll = poll_interval.as_log();
packet.mode = NtpMode::Request;

let client_cookie = NtpClientCookie::new_random();
packet.client_cookie = client_cookie;

(
packet,
RequestIdentifier {
expected_origin_timestamp: client_cookie.into_ntp_timestamp(),
uid: None,
},
)
}
}

#[cfg(test)]
Expand Down
Loading
Loading