Skip to main content

ntp/
extension.rs

1// Copyright 2026 U.S. Federal Government (in countries where recognized)
2// SPDX-License-Identifier: Apache-2.0
3
4//! NTP extension field parsing and NTS (Network Time Security) extension types.
5//!
6//! Extension fields follow the NTPv4 extension field format defined in RFC 7822,
7//! appended after the 48-byte NTP packet header. NTS (RFC 8915) defines specific
8//! extension field types for authenticated NTP.
9//!
10//! # Extension Field Format (RFC 7822)
11//!
12//! ```text
13//!  0                   1                   2                   3
14//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
15//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16//! |          Field Type           |        Field Length           |
17//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18//! .                                                               .
19//! .                       Field Value (variable)                  .
20//! .                                                               .
21//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22//! ```
23
24#[cfg(all(feature = "alloc", not(feature = "std")))]
25use alloc::vec;
26#[cfg(all(feature = "alloc", not(feature = "std")))]
27use alloc::vec::Vec;
28#[cfg(feature = "std")]
29use std::io;
30
31use crate::error::ParseError;
32
33/// A borrowed view of an extension field (no allocation).
34///
35/// This type references data within the original byte buffer, avoiding
36/// the heap allocation required by [`ExtensionField`].
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ExtensionFieldRef<'a> {
39    /// The extension field type code.
40    pub field_type: u16,
41    /// The extension field value (variable length, excluding the 4-byte header).
42    pub value: &'a [u8],
43}
44
45/// Iterator over extension fields in a byte buffer.
46///
47/// Yields [`ExtensionFieldRef`] values without heap allocation.
48/// Created by [`iter_extension_fields`].
49pub struct ExtensionFieldIter<'a> {
50    data: &'a [u8],
51    offset: usize,
52}
53
54impl<'a> Iterator for ExtensionFieldIter<'a> {
55    type Item = Result<ExtensionFieldRef<'a>, ParseError>;
56
57    fn next(&mut self) -> Option<Self::Item> {
58        let remaining = &self.data[self.offset..];
59        if remaining.len() < 4 {
60            return None;
61        }
62
63        let field_type = u16::from_be_bytes([remaining[0], remaining[1]]);
64        let field_length = u16::from_be_bytes([remaining[2], remaining[3]]);
65
66        if field_length < 4 {
67            return Some(Err(ParseError::InvalidExtensionLength {
68                declared: field_length,
69            }));
70        }
71
72        let value_length = (field_length - 4) as usize;
73        let value_start = self.offset + 4;
74
75        if value_start + value_length > self.data.len() {
76            return Some(Err(ParseError::ExtensionOverflow));
77        }
78
79        let value = &self.data[value_start..value_start + value_length];
80
81        // Advance past value and padding to 4-byte boundary.
82        let padded = (field_length as usize + 3) & !3;
83        let next_offset = self.offset + padded;
84        self.offset = next_offset.min(self.data.len());
85
86        Some(Ok(ExtensionFieldRef { field_type, value }))
87    }
88}
89
90/// Create an iterator over extension fields without allocating.
91///
92/// This is the zero-allocation alternative to [`parse_extension_fields`].
93/// Each item yields a borrowed view of the extension field data.
94pub fn iter_extension_fields(data: &[u8]) -> ExtensionFieldIter<'_> {
95    ExtensionFieldIter { data, offset: 0 }
96}
97
98/// Minimum extension field length per RFC 7822.
99pub const MIN_EXTENSION_FIELD_LENGTH: u16 = 16;
100
101// NTS extension field type codes (RFC 8915 Section 5.7).
102
103/// Unique Identifier extension field type.
104pub const UNIQUE_IDENTIFIER: u16 = 0x0104;
105
106/// NTS Cookie extension field type.
107pub const NTS_COOKIE: u16 = 0x0204;
108
109/// NTS Cookie Placeholder extension field type.
110pub const NTS_COOKIE_PLACEHOLDER: u16 = 0x0304;
111
112/// NTS Authenticator and Encrypted Extensions extension field type.
113pub const NTS_AUTHENTICATOR: u16 = 0x0404;
114
115/// A generic NTP extension field.
116#[cfg(any(feature = "alloc", feature = "std"))]
117#[derive(Clone, Debug, Eq, PartialEq)]
118pub struct ExtensionField {
119    /// The extension field type code.
120    pub field_type: u16,
121    /// The extension field value (variable length, excluding the 4-byte header).
122    pub value: Vec<u8>,
123}
124
125/// Parse extension fields from a byte buffer without using `std::io`.
126///
127/// Returns a vector of parsed extension fields. Stops when the remaining
128/// data is too short for another extension field header.
129#[cfg(any(feature = "alloc", feature = "std"))]
130pub fn parse_extension_fields_buf(data: &[u8]) -> Result<Vec<ExtensionField>, ParseError> {
131    iter_extension_fields(data)
132        .map(|r| {
133            r.map(|ef_ref| ExtensionField {
134                field_type: ef_ref.field_type,
135                value: ef_ref.value.to_vec(),
136            })
137        })
138        .collect()
139}
140
141/// Serialize extension fields into a byte buffer without using `std::io`.
142///
143/// Each field is padded to a 4-byte boundary with zero bytes.
144/// Returns the number of bytes written.
145#[cfg(any(feature = "alloc", feature = "std"))]
146pub fn write_extension_fields_buf(
147    fields: &[ExtensionField],
148    buf: &mut [u8],
149) -> Result<usize, ParseError> {
150    let mut offset = 0;
151
152    for field in fields {
153        let field_length = 4 + field.value.len();
154        let padded = (field_length + 3) & !3;
155
156        if offset + padded > buf.len() {
157            return Err(ParseError::BufferTooShort {
158                needed: offset + padded,
159                available: buf.len(),
160            });
161        }
162
163        let fl = field_length as u16;
164        buf[offset..offset + 2].copy_from_slice(&field.field_type.to_be_bytes());
165        buf[offset + 2..offset + 4].copy_from_slice(&fl.to_be_bytes());
166        buf[offset + 4..offset + 4 + field.value.len()].copy_from_slice(&field.value);
167
168        // Zero-fill padding.
169        for b in &mut buf[offset + field_length..offset + padded] {
170            *b = 0;
171        }
172
173        offset += padded;
174    }
175
176    Ok(offset)
177}
178
179/// Parse extension fields from data following the 48-byte NTP header.
180///
181/// Returns a vector of parsed extension fields. Stops when the remaining
182/// data is too short for another extension field header.
183#[cfg(feature = "std")]
184pub fn parse_extension_fields(data: &[u8]) -> io::Result<Vec<ExtensionField>> {
185    parse_extension_fields_buf(data).map_err(io::Error::from)
186}
187
188/// Serialize extension fields to a byte vector.
189///
190/// Each field is padded to a 4-byte boundary with zero bytes.
191#[cfg(feature = "std")]
192pub fn write_extension_fields(fields: &[ExtensionField]) -> io::Result<Vec<u8>> {
193    // Calculate total size needed.
194    let total: usize = fields.iter().map(|f| ((4 + f.value.len()) + 3) & !3).sum();
195    let mut buf = vec![0u8; total];
196    write_extension_fields_buf(fields, &mut buf)?;
197    Ok(buf)
198}
199
200/// NTS Unique Identifier extension field (RFC 8915 Section 5.3).
201///
202/// Contains random data for replay protection at the NTS level.
203/// The client generates this value and the server echoes it back.
204#[cfg(any(feature = "alloc", feature = "std"))]
205#[derive(Clone, Debug, Eq, PartialEq)]
206pub struct UniqueIdentifier(pub Vec<u8>);
207
208#[cfg(any(feature = "alloc", feature = "std"))]
209impl UniqueIdentifier {
210    /// Create a Unique Identifier from raw bytes.
211    pub fn new(data: Vec<u8>) -> Self {
212        UniqueIdentifier(data)
213    }
214
215    /// Convert to a generic extension field.
216    pub fn to_extension_field(&self) -> ExtensionField {
217        ExtensionField {
218            field_type: UNIQUE_IDENTIFIER,
219            value: self.0.clone(),
220        }
221    }
222
223    /// Try to extract from a generic extension field.
224    pub fn from_extension_field(ef: &ExtensionField) -> Option<Self> {
225        if ef.field_type == UNIQUE_IDENTIFIER {
226            Some(UniqueIdentifier(ef.value.clone()))
227        } else {
228            None
229        }
230    }
231}
232
233/// NTS Cookie extension field (RFC 8915 Section 5.4).
234///
235/// Contains an opaque cookie provided by the NTS-KE server.
236/// Each cookie is used exactly once per NTP request.
237#[cfg(any(feature = "alloc", feature = "std"))]
238#[derive(Clone, Debug, Eq, PartialEq)]
239pub struct NtsCookie(pub Vec<u8>);
240
241#[cfg(any(feature = "alloc", feature = "std"))]
242impl NtsCookie {
243    /// Create an NTS Cookie from raw bytes.
244    pub fn new(data: Vec<u8>) -> Self {
245        NtsCookie(data)
246    }
247
248    /// Convert to a generic extension field.
249    pub fn to_extension_field(&self) -> ExtensionField {
250        ExtensionField {
251            field_type: NTS_COOKIE,
252            value: self.0.clone(),
253        }
254    }
255
256    /// Try to extract from a generic extension field.
257    pub fn from_extension_field(ef: &ExtensionField) -> Option<Self> {
258        if ef.field_type == NTS_COOKIE {
259            Some(NtsCookie(ef.value.clone()))
260        } else {
261            None
262        }
263    }
264}
265
266/// NTS Cookie Placeholder extension field (RFC 8915 Section 5.5).
267///
268/// Signals to the server that the client wants to receive additional cookies.
269/// The placeholder size should match the expected cookie size.
270#[cfg(any(feature = "alloc", feature = "std"))]
271#[derive(Clone, Debug, Eq, PartialEq)]
272pub struct NtsCookiePlaceholder {
273    /// Size of the placeholder body in bytes.
274    pub size: usize,
275}
276
277#[cfg(any(feature = "alloc", feature = "std"))]
278impl NtsCookiePlaceholder {
279    /// Create a cookie placeholder of the given size.
280    pub fn new(size: usize) -> Self {
281        NtsCookiePlaceholder { size }
282    }
283
284    /// Convert to a generic extension field.
285    pub fn to_extension_field(&self) -> ExtensionField {
286        ExtensionField {
287            field_type: NTS_COOKIE_PLACEHOLDER,
288            value: vec![0u8; self.size],
289        }
290    }
291}
292
293/// NTS Authenticator and Encrypted Extensions extension field (RFC 8915 Section 5.6).
294///
295/// Contains the AEAD nonce and ciphertext. The ciphertext includes any
296/// encrypted extension fields plus the AEAD authentication tag.
297#[cfg(any(feature = "alloc", feature = "std"))]
298#[derive(Clone, Debug, Eq, PartialEq)]
299pub struct NtsAuthenticator {
300    /// The AEAD nonce.
301    pub nonce: Vec<u8>,
302    /// The AEAD ciphertext (encrypted extensions + authentication tag).
303    pub ciphertext: Vec<u8>,
304}
305
306#[cfg(any(feature = "alloc", feature = "std"))]
307impl NtsAuthenticator {
308    /// Create an NTS Authenticator.
309    pub fn new(nonce: Vec<u8>, ciphertext: Vec<u8>) -> Self {
310        NtsAuthenticator { nonce, ciphertext }
311    }
312
313    /// Convert to a generic extension field.
314    ///
315    /// The value format is: nonce_length (u16) + nonce + ciphertext_length (u16) + ciphertext.
316    pub fn to_extension_field(&self) -> ExtensionField {
317        let mut value = Vec::new();
318        // Nonce length (u16 BE) + nonce.
319        value.extend_from_slice(&(self.nonce.len() as u16).to_be_bytes());
320        value.extend_from_slice(&self.nonce);
321        // Pad nonce to 4-byte boundary.
322        let nonce_padded = (2 + self.nonce.len() + 3) & !3;
323        let nonce_pad = nonce_padded - (2 + self.nonce.len());
324        value.extend(core::iter::repeat_n(0u8, nonce_pad));
325        // Ciphertext length (u16 BE) + ciphertext.
326        value.extend_from_slice(&(self.ciphertext.len() as u16).to_be_bytes());
327        value.extend_from_slice(&self.ciphertext);
328
329        ExtensionField {
330            field_type: NTS_AUTHENTICATOR,
331            value,
332        }
333    }
334
335    /// Try to extract from a generic extension field.
336    #[cfg(feature = "std")]
337    pub fn from_extension_field(ef: &ExtensionField) -> io::Result<Option<Self>> {
338        Self::from_extension_field_buf(ef).map_err(io::Error::from)
339    }
340
341    /// Try to extract from a generic extension field without using `std::io`.
342    pub fn from_extension_field_buf(ef: &ExtensionField) -> Result<Option<Self>, ParseError> {
343        if ef.field_type != NTS_AUTHENTICATOR {
344            return Ok(None);
345        }
346
347        let data = &ef.value;
348        if data.len() < 2 {
349            return Err(ParseError::BufferTooShort {
350                needed: 2,
351                available: data.len(),
352            });
353        }
354
355        let nonce_len = u16::from_be_bytes([data[0], data[1]]) as usize;
356        let nonce_start = 2;
357
358        if nonce_start + nonce_len > data.len() {
359            return Err(ParseError::ExtensionOverflow);
360        }
361        let nonce = data[nonce_start..nonce_start + nonce_len].to_vec();
362
363        // Skip to padded boundary.
364        let nonce_padded = (2 + nonce_len + 3) & !3;
365        let ct_offset = nonce_padded;
366        if ct_offset + 2 > data.len() {
367            return Err(ParseError::BufferTooShort {
368                needed: ct_offset + 2,
369                available: data.len(),
370            });
371        }
372
373        let ct_len = u16::from_be_bytes([data[ct_offset], data[ct_offset + 1]]) as usize;
374        let ct_start = ct_offset + 2;
375
376        if ct_start + ct_len > data.len() {
377            return Err(ParseError::ExtensionOverflow);
378        }
379        let ciphertext = data[ct_start..ct_start + ct_len].to_vec();
380
381        Ok(Some(NtsAuthenticator { nonce, ciphertext }))
382    }
383}
384
385#[cfg(all(test, feature = "std"))]
386mod tests {
387    use super::*;
388
389    #[test]
390    fn test_parse_empty() {
391        let fields = parse_extension_fields(&[]).unwrap();
392        assert!(fields.is_empty());
393    }
394
395    #[test]
396    fn test_roundtrip_single_field() {
397        let field = ExtensionField {
398            field_type: UNIQUE_IDENTIFIER,
399            value: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
400        };
401        let buf = write_extension_fields(std::slice::from_ref(&field)).unwrap();
402        let parsed = parse_extension_fields(&buf).unwrap();
403        assert_eq!(parsed.len(), 1);
404        assert_eq!(parsed[0], field);
405    }
406
407    #[test]
408    fn test_roundtrip_multiple_fields() {
409        let fields = vec![
410            ExtensionField {
411                field_type: UNIQUE_IDENTIFIER,
412                value: vec![0xAA; 32],
413            },
414            ExtensionField {
415                field_type: NTS_COOKIE,
416                value: vec![0xBB; 64],
417            },
418        ];
419        let buf = write_extension_fields(&fields).unwrap();
420        let parsed = parse_extension_fields(&buf).unwrap();
421        assert_eq!(parsed.len(), 2);
422        assert_eq!(parsed[0], fields[0]);
423        assert_eq!(parsed[1], fields[1]);
424    }
425
426    #[test]
427    fn test_padding() {
428        // Value of 5 bytes: 4 header + 5 value = 9 bytes, padded to 12.
429        let field = ExtensionField {
430            field_type: 0x1234,
431            value: vec![1, 2, 3, 4, 5],
432        };
433        let buf = write_extension_fields(&[field]).unwrap();
434        assert_eq!(buf.len(), 12); // 4 header + 5 value + 3 padding
435    }
436
437    #[test]
438    fn test_unique_identifier_conversion() {
439        let uid = UniqueIdentifier::new(vec![0x42; 32]);
440        let ef = uid.to_extension_field();
441        assert_eq!(ef.field_type, UNIQUE_IDENTIFIER);
442        let back = UniqueIdentifier::from_extension_field(&ef).unwrap();
443        assert_eq!(back.0, vec![0x42; 32]);
444    }
445
446    #[test]
447    fn test_nts_cookie_conversion() {
448        let cookie = NtsCookie::new(vec![0xDE, 0xAD, 0xBE, 0xEF]);
449        let ef = cookie.to_extension_field();
450        assert_eq!(ef.field_type, NTS_COOKIE);
451        let back = NtsCookie::from_extension_field(&ef).unwrap();
452        assert_eq!(back.0, vec![0xDE, 0xAD, 0xBE, 0xEF]);
453    }
454
455    #[test]
456    fn test_nts_authenticator_roundtrip() {
457        let auth = NtsAuthenticator::new(vec![0x11; 16], vec![0x22; 48]);
458        let ef = auth.to_extension_field();
459        assert_eq!(ef.field_type, NTS_AUTHENTICATOR);
460        let back = NtsAuthenticator::from_extension_field(&ef)
461            .unwrap()
462            .unwrap();
463        assert_eq!(back.nonce, vec![0x11; 16]);
464        assert_eq!(back.ciphertext, vec![0x22; 48]);
465    }
466
467    #[test]
468    fn test_cookie_placeholder() {
469        let placeholder = NtsCookiePlaceholder::new(100);
470        let ef = placeholder.to_extension_field();
471        assert_eq!(ef.field_type, NTS_COOKIE_PLACEHOLDER);
472        assert_eq!(ef.value.len(), 100);
473        assert!(ef.value.iter().all(|&b| b == 0));
474    }
475
476    #[test]
477    fn test_parse_truncated_field() {
478        // Only 3 bytes: not enough for the 4-byte header.
479        let data = [0x01, 0x04, 0x00];
480        let fields = parse_extension_fields(&data).unwrap();
481        assert!(fields.is_empty()); // Silently stops, not enough for header
482    }
483
484    #[test]
485    fn test_parse_invalid_length() {
486        // field_length=2 (less than 4).
487        let data = [0x01, 0x04, 0x00, 0x02];
488        let result = parse_extension_fields(&data);
489        assert!(result.is_err());
490    }
491
492    // Buffer-based API tests.
493
494    #[test]
495    fn test_buf_parse_empty() {
496        let fields = parse_extension_fields_buf(&[]).unwrap();
497        assert!(fields.is_empty());
498    }
499
500    #[test]
501    fn test_buf_roundtrip_single_field() {
502        let field = ExtensionField {
503            field_type: UNIQUE_IDENTIFIER,
504            value: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
505        };
506
507        // Write to Vec via io API, then to fixed buffer via buf API.
508        let io_buf = write_extension_fields(std::slice::from_ref(&field)).unwrap();
509        let mut buf = vec![0u8; 256];
510        let written = write_extension_fields_buf(std::slice::from_ref(&field), &mut buf).unwrap();
511        assert_eq!(&io_buf[..], &buf[..written]);
512
513        // Parse with buf API.
514        let parsed = parse_extension_fields_buf(&buf[..written]).unwrap();
515        assert_eq!(parsed.len(), 1);
516        assert_eq!(parsed[0], field);
517    }
518
519    #[test]
520    fn test_buf_equivalence_with_io_api() {
521        let fields = vec![
522            ExtensionField {
523                field_type: UNIQUE_IDENTIFIER,
524                value: vec![0xAA; 32],
525            },
526            ExtensionField {
527                field_type: NTS_COOKIE,
528                value: vec![0xBB; 64],
529            },
530        ];
531
532        let io_buf = write_extension_fields(&fields).unwrap();
533        let mut raw_buf = vec![0u8; 512];
534        let written = write_extension_fields_buf(&fields, &mut raw_buf).unwrap();
535
536        // Same output.
537        assert_eq!(&io_buf[..], &raw_buf[..written]);
538
539        // Same parse result.
540        let io_parsed = parse_extension_fields(&io_buf).unwrap();
541        let buf_parsed = parse_extension_fields_buf(&raw_buf[..written]).unwrap();
542        assert_eq!(io_parsed, buf_parsed);
543    }
544
545    #[test]
546    fn test_buf_write_buffer_too_short() {
547        let field = ExtensionField {
548            field_type: UNIQUE_IDENTIFIER,
549            value: vec![0xAA; 32],
550        };
551        let mut tiny_buf = [0u8; 4]; // Too small for 4 header + 32 value.
552        let result = write_extension_fields_buf(&[field], &mut tiny_buf);
553        assert!(result.is_err());
554    }
555
556    #[test]
557    fn test_buf_parse_invalid_length() {
558        let data = [0x01, 0x04, 0x00, 0x02]; // field_length=2 (< 4).
559        let result = parse_extension_fields_buf(&data);
560        assert!(matches!(
561            result,
562            Err(ParseError::InvalidExtensionLength { declared: 2 })
563        ));
564    }
565
566    #[test]
567    fn test_iter_extension_fields() {
568        let fields = vec![
569            ExtensionField {
570                field_type: UNIQUE_IDENTIFIER,
571                value: vec![0xAA; 32],
572            },
573            ExtensionField {
574                field_type: NTS_COOKIE,
575                value: vec![0xBB; 64],
576            },
577        ];
578        let io_buf = write_extension_fields(&fields).unwrap();
579
580        let mut iter = iter_extension_fields(&io_buf);
581
582        let first = iter.next().unwrap().unwrap();
583        assert_eq!(first.field_type, UNIQUE_IDENTIFIER);
584        assert_eq!(first.value, &[0xAA; 32][..]);
585
586        let second = iter.next().unwrap().unwrap();
587        assert_eq!(second.field_type, NTS_COOKIE);
588        assert_eq!(second.value, &[0xBB; 64][..]);
589
590        assert!(iter.next().is_none());
591    }
592
593    #[test]
594    fn test_iter_extension_fields_empty() {
595        let mut iter = iter_extension_fields(&[]);
596        assert!(iter.next().is_none());
597    }
598
599    #[test]
600    fn test_nts_authenticator_buf_roundtrip() {
601        let auth = NtsAuthenticator::new(vec![0x11; 16], vec![0x22; 48]);
602        let ef = auth.to_extension_field();
603        let back = NtsAuthenticator::from_extension_field_buf(&ef)
604            .unwrap()
605            .unwrap();
606        assert_eq!(back.nonce, vec![0x11; 16]);
607        assert_eq!(back.ciphertext, vec![0x22; 48]);
608    }
609}