Skip to main content

detcore_model/
liteinst_native_exec_input.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 * This source code is licensed under the BSD-style license found in LICENSE.
5 */
6
7//! Separate, inactive native Initial/Successor bootstrap input data.
8//!
9//! No decoder fallback to HLIINIT1 exists. Packet bytes, socket identities,
10//! nonces, tickets and digests are not admission or kernel-exec authority. A
11//! future trusted native consumer must compare independently held expectations,
12//! verify distinct protected actual sockets, and redeem its one-use instance.
13//! The runtime/Tool composite is delivered separately on the bounded state path.
14
15use std::fmt;
16
17/// Entire control packet bound, unchanged from the existing private input ABI.
18pub const MAX_PACKET_LEN: usize = 4096;
19/// A protocol distinct from the unchanged HLIINIT1 packet.
20pub const MAGIC: [u8; 8] = *b"HLNEXEC1";
21/// Only this wire version is admitted by the shape decoder.
22pub const VERSION: u16 = 1;
23/// Initial header, including all four variable-field lengths.
24pub const INITIAL_HEADER_LEN: usize = 200;
25/// Successor header additionally carries the composite state digest.
26pub const SUCCESSOR_HEADER_LEN: usize = 232;
27const TOKEN_START: usize = 160;
28const TOKEN_END: usize = 192;
29const LENGTH_BYTES: usize = 8;
30
31/// The selected input shape; not a native lifecycle authorization.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33#[repr(u16)]
34pub enum Kind {
35    /// Fresh one-use native launch instance.
36    Initial = 1,
37    /// One-use native exec successor transaction.
38    Successor = 2,
39}
40
41/// Distinct fixed fields for the two native input kinds.
42#[derive(Clone, Copy, Eq, PartialEq)]
43pub enum Image<'a> {
44    /// Independently issued nonce, to be consumed by actual launch admission.
45    Initial {
46        /// Thirty-two opaque bytes, with no replay or authority check here.
47        nonce: &'a [u8; 32],
48    },
49    /// Independently issued ticket and expected exact composite-state digest.
50    Successor {
51        /// Thirty-two opaque bytes, consumed before Config/state delivery.
52        ticket: &'a [u8; 32],
53        /// SHA256 of the entire exact Tool/runtime composite, not a prefix.
54        composite_digest: &'a [u8; 32],
55    },
56}
57impl fmt::Debug for Image<'_> {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        // Shape diagnostics must not print a protected nonce or exec ticket.
60        match self {
61            Self::Initial { .. } => f.write_str("Initial(<private nonce>)"),
62            Self::Successor {
63                composite_digest, ..
64            } => f
65                .debug_struct("Successor")
66                .field("ticket", &"<private>")
67                .field("composite_digest", composite_digest)
68                .finish(),
69        }
70    }
71}
72impl Image<'_> {
73    /// Exact wire kind corresponding to these fields.
74    pub fn kind(&self) -> Kind {
75        match self {
76            Self::Initial { .. } => Kind::Initial,
77            Self::Successor { .. } => Kind::Successor,
78        }
79    }
80    fn header_len(&self) -> usize {
81        match self {
82            Self::Initial { .. } => INITIAL_HEADER_LEN,
83            Self::Successor { .. } => SUCCESSOR_HEADER_LEN,
84        }
85    }
86}
87
88/// Borrowed native control fields. Decoding establishes byte shape only.
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct Packet<'a> {
91    /// Canonical legacy wire Config SHA256 supplied independently by admission.
92    pub config_digest: &'a [u8; 32],
93    /// Existing exactly sixteen ASCII-hex Config schema characters.
94    pub config_schema: &'a [u8; 16],
95    /// Full declared Rust native source SHA256, independently computed.
96    pub native_source_digest: &'a [u8; 32],
97    /// Held immutable target identity; bytes here do not prove its file.
98    pub target_digest: &'a [u8; 32],
99    /// Held native bootstrap/interpreter identity; not exec attestation.
100    pub bootstrap_digest: &'a [u8; 32],
101    /// One of the distinct nonce or ticket/composite input shapes.
102    pub image: Image<'a>,
103    /// Nonempty NUL-free raw Unix coordinator path; UTF-8 is not required.
104    pub coordinator: &'a [u8],
105    /// Unchanged UTF-8 DETLOG fd/device/inode identity string contract.
106    pub detlog_identity: &'a str,
107    /// Unchanged UTF-8 genuine forwarding policy, compared by the consumer.
108    pub forward_policy: &'a str,
109    /// Separate UTF-8 inherited observer fd/device/inode identity data. The
110    /// consumer must verify its actual socket is distinct and protected; string
111    /// inequality is insufficient and this codec does not query descriptors.
112    pub observer_identity: &'a str,
113}
114
115/// Independently held expected data, not values learned from packet decoding.
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct ExpectedBindings<'a> {
118    /// Canonical full wire Config SHA256.
119    pub config_digest: &'a [u8; 32],
120    /// Actual compiled Config schema, compared byte-for-byte, never by prefix.
121    pub config_schema: &'a [u8; 16],
122    /// Full actual compiled native source digest.
123    pub native_source_digest: &'a [u8; 32],
124    /// Independently held immutable target digest.
125    pub target_digest: &'a [u8; 32],
126    /// Independently held bootstrap/interpreter digest.
127    pub bootstrap_digest: &'a [u8; 32],
128}
129
130/// Exact data disagreement. No variant is a kernel errno or authorization.
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub enum BindingMismatch {
133    ConfigDigest,
134    ConfigSchema,
135    NativeSourceDigest,
136    TargetDigest,
137    BootstrapDigest,
138}
139impl Packet<'_> {
140    /// Compare complete fixed bindings against independent expected inputs.
141    /// This neither verifies sockets nor redeems the nonce/ticket, checks the
142    /// composite state bytes, or establishes a watched birth/exec transition.
143    pub fn check_bindings(&self, expected: ExpectedBindings<'_>) -> Result<(), BindingMismatch> {
144        for (actual, expected, error) in [
145            (
146                self.config_digest,
147                expected.config_digest,
148                BindingMismatch::ConfigDigest,
149            ),
150            (
151                self.native_source_digest,
152                expected.native_source_digest,
153                BindingMismatch::NativeSourceDigest,
154            ),
155            (
156                self.target_digest,
157                expected.target_digest,
158                BindingMismatch::TargetDigest,
159            ),
160            (
161                self.bootstrap_digest,
162                expected.bootstrap_digest,
163                BindingMismatch::BootstrapDigest,
164            ),
165        ] {
166            if actual != expected {
167                return Err(error);
168            }
169        }
170        if self.config_schema != expected.config_schema {
171            return Err(BindingMismatch::ConfigSchema);
172        }
173        Ok(())
174    }
175    fn variable_fields(&self) -> [&[u8]; 4] {
176        [
177            self.coordinator,
178            self.detlog_identity.as_bytes(),
179            self.forward_policy.as_bytes(),
180            self.observer_identity.as_bytes(),
181        ]
182    }
183}
184
185/// Bounded shape error with no allocated error message.
186#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub enum PacketError {
188    Size,
189    Magic,
190    Version,
191    Kind,
192    Reserved,
193    Length,
194    Empty,
195    Nul,
196    Utf8,
197    Schema,
198}
199impl fmt::Display for PacketError {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.write_str(match self {
202            Self::Size => "native input complete size mismatch",
203            Self::Magic => "native input magic mismatch",
204            Self::Version => "native input version mismatch",
205            Self::Kind => "native input kind mismatch",
206            Self::Reserved => "native input reserved bits are nonzero",
207            Self::Length => "native input field lengths do not consume exactly",
208            Self::Empty => "native input mandatory field is empty",
209            Self::Nul => "native input field contains NUL",
210            Self::Utf8 => "native input identity or policy is not UTF-8",
211            Self::Schema => "native input Config schema is not sixteen ASCII hex characters",
212        })
213    }
214}
215impl std::error::Error for PacketError {}
216
217fn word(bytes: &[u8], start: usize) -> usize {
218    usize::from(u16::from_be_bytes([bytes[start], bytes[start + 1]]))
219}
220fn check_field(bytes: &[u8]) -> Result<(), PacketError> {
221    if bytes.is_empty() {
222        return Err(PacketError::Empty);
223    }
224    if bytes.contains(&0) {
225        return Err(PacketError::Nul);
226    }
227    Ok(())
228}
229fn check_schema(bytes: &[u8; 16]) -> Result<(), PacketError> {
230    if bytes.iter().all(u8::is_ascii_hexdigit) {
231        Ok(())
232    } else {
233        Err(PacketError::Schema)
234    }
235}
236
237/// Validate the complete new packet shape without allocation or old fallback.
238pub fn decode(bytes: &[u8]) -> Result<Packet<'_>, PacketError> {
239    if !(INITIAL_HEADER_LEN..=MAX_PACKET_LEN).contains(&bytes.len()) {
240        return Err(PacketError::Size);
241    }
242    if bytes[..8] != MAGIC {
243        return Err(PacketError::Magic);
244    }
245    if word(bytes, 8) != usize::from(VERSION) {
246        return Err(PacketError::Version);
247    }
248    if word(bytes, 10) != bytes.len() {
249        return Err(PacketError::Size);
250    }
251    let kind = match word(bytes, 12) {
252        1 => Kind::Initial,
253        2 => Kind::Successor,
254        _ => return Err(PacketError::Kind),
255    };
256    if word(bytes, 14) != 0 {
257        return Err(PacketError::Reserved);
258    }
259    let header = match kind {
260        Kind::Initial => INITIAL_HEADER_LEN,
261        Kind::Successor => SUCCESSOR_HEADER_LEN,
262    };
263    if bytes.len() < header {
264        return Err(PacketError::Size);
265    }
266    let lengths_start = header - LENGTH_BYTES;
267    let mut ends = [header; 5];
268    for index in 0..4 {
269        ends[index + 1] = ends[index]
270            .checked_add(word(bytes, lengths_start + index * 2))
271            .filter(|end| *end <= bytes.len())
272            .ok_or(PacketError::Length)?;
273    }
274    if ends[4] != bytes.len() {
275        return Err(PacketError::Length);
276    }
277    let fields = std::array::from_fn::<_, 4, _>(|index| &bytes[ends[index]..ends[index + 1]]);
278    for field in fields {
279        check_field(field)?;
280    }
281    let token = bytes[TOKEN_START..TOKEN_END]
282        .try_into()
283        .map_err(|_| PacketError::Length)?;
284    let image = match kind {
285        Kind::Initial => Image::Initial { nonce: token },
286        Kind::Successor => Image::Successor {
287            ticket: token,
288            composite_digest: bytes[TOKEN_END..TOKEN_END + 32]
289                .try_into()
290                .map_err(|_| PacketError::Length)?,
291        },
292    };
293    let packet = Packet {
294        config_digest: bytes[16..48].try_into().map_err(|_| PacketError::Length)?,
295        config_schema: bytes[48..64].try_into().map_err(|_| PacketError::Schema)?,
296        native_source_digest: bytes[64..96].try_into().map_err(|_| PacketError::Length)?,
297        target_digest: bytes[96..128].try_into().map_err(|_| PacketError::Length)?,
298        bootstrap_digest: bytes[128..160]
299            .try_into()
300            .map_err(|_| PacketError::Length)?,
301        image,
302        coordinator: fields[0],
303        detlog_identity: std::str::from_utf8(fields[1]).map_err(|_| PacketError::Utf8)?,
304        forward_policy: std::str::from_utf8(fields[2]).map_err(|_| PacketError::Utf8)?,
305        observer_identity: std::str::from_utf8(fields[3]).map_err(|_| PacketError::Utf8)?,
306    };
307    check_schema(packet.config_schema)?;
308    Ok(packet)
309}
310
311/// Host-side encoding of the same shape. State bytes do not enter this packet.
312pub fn encode(packet: Packet<'_>) -> Result<Vec<u8>, PacketError> {
313    let fields = packet.variable_fields();
314    let mut total = packet.image.header_len();
315    for field in fields {
316        if field.len() > MAX_PACKET_LEN {
317            return Err(PacketError::Size);
318        }
319        check_field(field)?;
320        total = total
321            .checked_add(field.len())
322            .filter(|total| *total <= MAX_PACKET_LEN)
323            .ok_or(PacketError::Size)?;
324    }
325    check_schema(packet.config_schema)?;
326    let mut bytes = Vec::with_capacity(total);
327    bytes.extend_from_slice(&MAGIC);
328    bytes.extend_from_slice(&VERSION.to_be_bytes());
329    bytes.extend_from_slice(&(total as u16).to_be_bytes());
330    bytes.extend_from_slice(&(packet.image.kind() as u16).to_be_bytes());
331    bytes.extend_from_slice(&0_u16.to_be_bytes());
332    bytes.extend_from_slice(packet.config_digest);
333    bytes.extend_from_slice(packet.config_schema);
334    bytes.extend_from_slice(packet.native_source_digest);
335    bytes.extend_from_slice(packet.target_digest);
336    bytes.extend_from_slice(packet.bootstrap_digest);
337    match packet.image {
338        Image::Initial { nonce } => bytes.extend_from_slice(nonce),
339        Image::Successor {
340            ticket,
341            composite_digest,
342        } => {
343            bytes.extend_from_slice(ticket);
344            bytes.extend_from_slice(composite_digest);
345        }
346    }
347    for field in fields {
348        bytes.extend_from_slice(&(field.len() as u16).to_be_bytes());
349    }
350    for field in fields {
351        bytes.extend_from_slice(field);
352    }
353    Ok(bytes)
354}
355
356/// Encode into caller-owned storage with exactly the same wire bytes as
357/// `encode`. Bounds and mandatory fields are checked before the first write.
358/// No allocation or packet-supplied authentication is performed.
359pub fn encode_into(packet: Packet<'_>, output: &mut [u8]) -> Result<usize, PacketError> {
360    let fields = packet.variable_fields();
361    let mut total = packet.image.header_len();
362    for field in fields {
363        if field.len() > MAX_PACKET_LEN {
364            return Err(PacketError::Size);
365        }
366        check_field(field)?;
367        total = total
368            .checked_add(field.len())
369            .filter(|total| *total <= MAX_PACKET_LEN)
370            .ok_or(PacketError::Size)?;
371    }
372    check_schema(packet.config_schema)?;
373    if output.len() < total {
374        return Err(PacketError::Size);
375    }
376    let mut offset = 0;
377    let mut append = |bytes: &[u8]| {
378        output[offset..offset + bytes.len()].copy_from_slice(bytes);
379        offset += bytes.len();
380    };
381    append(&MAGIC);
382    append(&VERSION.to_be_bytes());
383    append(&(total as u16).to_be_bytes());
384    append(&(packet.image.kind() as u16).to_be_bytes());
385    append(&0_u16.to_be_bytes());
386    append(packet.config_digest);
387    append(packet.config_schema);
388    append(packet.native_source_digest);
389    append(packet.target_digest);
390    append(packet.bootstrap_digest);
391    match packet.image {
392        Image::Initial { nonce } => append(nonce),
393        Image::Successor {
394            ticket,
395            composite_digest,
396        } => {
397            append(ticket);
398            append(composite_digest);
399        }
400    }
401    for field in fields {
402        append(&(field.len() as u16).to_be_bytes());
403    }
404    for field in fields {
405        append(field);
406    }
407    if offset != total {
408        return Err(PacketError::Length);
409    }
410    Ok(total)
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416
417    fn packet(successor: bool) -> Packet<'static> {
418        Packet {
419            config_digest: &[2; 32],
420            config_schema: b"0123456789abcdef",
421            native_source_digest: &[3; 32],
422            target_digest: &[4; 32],
423            bootstrap_digest: &[5; 32],
424            image: if successor {
425                Image::Successor {
426                    ticket: &[6; 32],
427                    composite_digest: &[7; 32],
428                }
429            } else {
430                Image::Initial { nonce: &[1; 32] }
431            },
432            coordinator: b"/tmp/native-\xff.sock",
433            detlog_identity: "209:0000000000000007:0000000000001234",
434            forward_policy: "INFO",
435            observer_identity: "210:0000000000000007:0000000000005678",
436        }
437    }
438    fn expected(packet: Packet<'_>) -> ExpectedBindings<'_> {
439        ExpectedBindings {
440            config_digest: packet.config_digest,
441            config_schema: packet.config_schema,
442            native_source_digest: packet.native_source_digest,
443            target_digest: packet.target_digest,
444            bootstrap_digest: packet.bootstrap_digest,
445        }
446    }
447    fn advertised(bytes: &mut [u8]) {
448        let total = bytes.len() as u16;
449        bytes[10..12].copy_from_slice(&total.to_be_bytes());
450    }
451    fn field_start(packet: Packet<'_>, index: usize) -> usize {
452        packet.image.header_len()
453            + packet.variable_fields()[..index]
454                .iter()
455                .map(|field| field.len())
456                .sum::<usize>()
457    }
458
459    #[test]
460    fn exact_layout_both_kinds_roundtrip_and_borrow_all_fields() {
461        assert_eq!(MAX_PACKET_LEN, 4096);
462        assert_eq!(INITIAL_HEADER_LEN, 200);
463        assert_eq!(SUCCESSOR_HEADER_LEN, 232);
464        for successor in [false, true] {
465            let original = packet(successor);
466            let bytes = encode(original).unwrap();
467            assert_eq!(&bytes[..8], b"HLNEXEC1");
468            assert_eq!(&bytes[8..10], &[0, 1]);
469            assert_eq!(word(&bytes, 10), bytes.len());
470            assert_eq!(word(&bytes, 12), if successor { 2 } else { 1 });
471            assert_eq!(&bytes[14..16], &[0, 0]);
472            for (range, wanted) in [
473                (16..48, original.config_digest.as_slice()),
474                (48..64, original.config_schema.as_slice()),
475                (64..96, original.native_source_digest.as_slice()),
476                (96..128, original.target_digest.as_slice()),
477                (128..160, original.bootstrap_digest.as_slice()),
478                (160..192, if successor { &[6; 32] } else { &[1; 32] }),
479            ] {
480                assert_eq!(&bytes[range], wanted);
481            }
482            if successor {
483                assert_eq!(&bytes[192..224], &[7; 32]);
484            }
485            let header = original.image.header_len();
486            for (index, field) in original.variable_fields().into_iter().enumerate() {
487                assert_eq!(word(&bytes, header - LENGTH_BYTES + 2 * index), field.len());
488                let start = field_start(original, index);
489                assert_eq!(&bytes[start..start + field.len()], field);
490            }
491            let decoded = decode(&bytes).unwrap();
492            assert_eq!(decoded, original);
493            assert_eq!(decoded.config_digest.as_ptr(), bytes[16..].as_ptr());
494            assert_eq!(decoded.config_schema.as_ptr(), bytes[48..].as_ptr());
495            assert_eq!(decoded.native_source_digest.as_ptr(), bytes[64..].as_ptr());
496            assert_eq!(decoded.target_digest.as_ptr(), bytes[96..].as_ptr());
497            assert_eq!(decoded.bootstrap_digest.as_ptr(), bytes[128..].as_ptr());
498            assert_eq!(decoded.coordinator.as_ptr(), bytes[header..].as_ptr());
499            assert_eq!(
500                decoded.detlog_identity.as_ptr(),
501                bytes[field_start(original, 1)..].as_ptr()
502            );
503            assert_eq!(
504                decoded.forward_policy.as_ptr(),
505                bytes[field_start(original, 2)..].as_ptr()
506            );
507            assert_eq!(
508                decoded.observer_identity.as_ptr(),
509                bytes[field_start(original, 3)..].as_ptr()
510            );
511            match decoded.image {
512                Image::Initial { nonce } => assert_eq!(nonce.as_ptr(), bytes[160..].as_ptr()),
513                Image::Successor {
514                    ticket,
515                    composite_digest,
516                } => {
517                    assert_eq!(ticket.as_ptr(), bytes[160..].as_ptr());
518                    assert_eq!(composite_digest.as_ptr(), bytes[192..].as_ptr());
519                }
520            }
521            assert_eq!(decoded.check_bindings(expected(original)), Ok(()));
522        }
523    }
524
525    #[test]
526    fn every_truncated_prefix_and_wrong_complete_size_refuse() {
527        for successor in [false, true] {
528            let valid = encode(packet(successor)).unwrap();
529            for length in 0..valid.len() {
530                assert!(
531                    decode(&valid[..length]).is_err(),
532                    "kind={successor} prefix={length}"
533                );
534                if length >= 12 {
535                    let mut short = valid[..length].to_vec();
536                    advertised(&mut short);
537                    assert!(
538                        decode(&short).is_err(),
539                        "advertised kind={successor} prefix={length}"
540                    );
541                }
542            }
543            let mut wrong = valid.clone();
544            wrong[10..12].copy_from_slice(&0_u16.to_be_bytes());
545            assert_eq!(decode(&wrong), Err(PacketError::Size));
546            wrong[10..12].copy_from_slice(&4096_u16.to_be_bytes());
547            assert_eq!(decode(&wrong), Err(PacketError::Size));
548            let mut extra = valid;
549            extra.push(9);
550            assert_eq!(decode(&extra), Err(PacketError::Size));
551            advertised(&mut extra);
552            assert_eq!(decode(&extra), Err(PacketError::Length));
553        }
554    }
555
556    #[test]
557    fn magic_version_kind_reserved_and_every_length_are_exact() {
558        for successor in [false, true] {
559            let original = packet(successor);
560            let valid = encode(original).unwrap();
561            for index in 0..8 {
562                let mut changed = valid.clone();
563                changed[index] ^= 1;
564                assert_eq!(decode(&changed), Err(PacketError::Magic));
565            }
566            for version in [0_u16, 2, u16::MAX] {
567                let mut changed = valid.clone();
568                changed[8..10].copy_from_slice(&version.to_be_bytes());
569                assert_eq!(decode(&changed), Err(PacketError::Version));
570            }
571            for kind in [0_u16, 3, u16::MAX] {
572                let mut changed = valid.clone();
573                changed[12..14].copy_from_slice(&kind.to_be_bytes());
574                assert_eq!(decode(&changed), Err(PacketError::Kind));
575            }
576            for index in [14, 15] {
577                let mut changed = valid.clone();
578                changed[index] = 1;
579                assert_eq!(decode(&changed), Err(PacketError::Reserved));
580            }
581            let lengths = original.image.header_len() - LENGTH_BYTES;
582            for field in 0..4 {
583                let index = lengths + 2 * field;
584                for length in [
585                    word(&valid, index) - 1,
586                    word(&valid, index) + 1,
587                    u16::MAX as usize,
588                ] {
589                    let mut changed = valid.clone();
590                    changed[index..index + 2].copy_from_slice(&(length as u16).to_be_bytes());
591                    assert_eq!(
592                        decode(&changed),
593                        Err(PacketError::Length),
594                        "kind={successor} field={field} length={length}"
595                    );
596                }
597            }
598        }
599    }
600
601    #[test]
602    fn every_empty_nul_and_nonutf8_text_field_refuses_raw_path_stays_raw() {
603        for successor in [false, true] {
604            let original = packet(successor);
605            let valid = encode(original).unwrap();
606            assert!(std::str::from_utf8(original.coordinator).is_err());
607            assert_eq!(decode(&valid).unwrap().coordinator, original.coordinator);
608            for index in 0..4 {
609                let start = field_start(original, index);
610                let length = original.variable_fields()[index].len();
611                let mut empty = valid.clone();
612                empty.drain(start..start + length);
613                let lengths = original.image.header_len() - LENGTH_BYTES + 2 * index;
614                empty[lengths..lengths + 2].copy_from_slice(&0_u16.to_be_bytes());
615                advertised(&mut empty);
616                assert_eq!(decode(&empty), Err(PacketError::Empty), "field {index}");
617                let mut nul = valid.clone();
618                nul[start] = 0;
619                assert_eq!(decode(&nul), Err(PacketError::Nul), "field {index}");
620                if index != 0 {
621                    let mut nonutf8 = valid.clone();
622                    nonutf8[start] = 0xff;
623                    assert_eq!(decode(&nonutf8), Err(PacketError::Utf8), "field {index}");
624                }
625            }
626        }
627    }
628
629    #[test]
630    fn exact_bound_both_kinds_and_each_host_validation_refuse_without_widening() {
631        for successor in [false, true] {
632            let mut original = packet(successor);
633            let used = original.image.header_len()
634                + original.variable_fields()[1..]
635                    .iter()
636                    .map(|field| field.len())
637                    .sum::<usize>();
638            let path = vec![b'x'; MAX_PACKET_LEN - used];
639            original.coordinator = &path;
640            let full = encode(original).unwrap();
641            assert_eq!(full.len(), 4096);
642            assert_eq!(decode(&full).unwrap(), original);
643            let oversized = vec![b'x'; path.len() + 1];
644            original.coordinator = &oversized;
645            assert_eq!(encode(original), Err(PacketError::Size));
646            assert_eq!(decode(&vec![1; 4097]), Err(PacketError::Size));
647            for field in 0..4 {
648                for empty in [false, true] {
649                    let value = if empty { "" } else { "x\0" };
650                    let mut invalid = packet(successor);
651                    match field {
652                        0 => invalid.coordinator = value.as_bytes(),
653                        1 => invalid.detlog_identity = value,
654                        2 => invalid.forward_policy = value,
655                        3 => invalid.observer_identity = value,
656                        _ => unreachable!(),
657                    }
658                    assert_eq!(
659                        encode(invalid),
660                        Err(if empty {
661                            PacketError::Empty
662                        } else {
663                            PacketError::Nul
664                        })
665                    );
666                }
667            }
668        }
669    }
670
671    #[test]
672    fn schema_all_positions_are_ascii_hex_and_expected_match_never_uses_a_prefix() {
673        for successor in [false, true] {
674            let original = packet(successor);
675            let valid = encode(original).unwrap();
676            for index in 0..16 {
677                for byte in [b'g', 0, 0xff, b'/', b':'] {
678                    let mut changed = valid.clone();
679                    changed[48 + index] = byte;
680                    assert_eq!(decode(&changed), Err(PacketError::Schema));
681                    let schema: &[u8; 16] = changed[48..64].try_into().unwrap();
682                    let mut packet = original;
683                    packet.config_schema = schema;
684                    assert_eq!(encode(packet), Err(PacketError::Schema));
685                }
686            }
687            let uppercase = *b"0123456789ABCDEF";
688            let mut packet = original;
689            packet.config_schema = &uppercase;
690            let bytes = encode(packet).unwrap();
691            assert_eq!(
692                decode(&bytes).unwrap().check_bindings(expected(original)),
693                Err(BindingMismatch::ConfigSchema)
694            );
695            let mut last = *original.config_schema;
696            last[15] = b'0';
697            assert_eq!(&last[..15], &original.config_schema[..15]);
698            let mut independent = expected(original);
699            independent.config_schema = &last;
700            assert_eq!(
701                decode(&valid).unwrap().check_bindings(independent),
702                Err(BindingMismatch::ConfigSchema)
703            );
704        }
705    }
706
707    #[test]
708    fn all_digest_bytes_compare_exactly_with_independent_expectations() {
709        for successor in [false, true] {
710            let original = packet(successor);
711            let bytes = encode(original).unwrap();
712            let decoded = decode(&bytes).unwrap();
713            for field in 0..4 {
714                for index in 0..32 {
715                    let mut changed = *[
716                        original.config_digest,
717                        original.native_source_digest,
718                        original.target_digest,
719                        original.bootstrap_digest,
720                    ][field];
721                    changed[index] ^= 1;
722                    let mut bindings = expected(original);
723                    let error = match field {
724                        0 => {
725                            bindings.config_digest = &changed;
726                            BindingMismatch::ConfigDigest
727                        }
728                        1 => {
729                            bindings.native_source_digest = &changed;
730                            BindingMismatch::NativeSourceDigest
731                        }
732                        2 => {
733                            bindings.target_digest = &changed;
734                            BindingMismatch::TargetDigest
735                        }
736                        3 => {
737                            bindings.bootstrap_digest = &changed;
738                            BindingMismatch::BootstrapDigest
739                        }
740                        _ => unreachable!(),
741                    };
742                    assert_eq!(
743                        decoded.check_bindings(bindings),
744                        Err(error),
745                        "field={field} byte={index}"
746                    );
747                }
748            }
749        }
750    }
751
752    #[test]
753    fn kinds_have_distinct_token_shapes_and_no_hliinit1_reader_fallback() {
754        for successor in [false, true] {
755            let valid = encode(packet(successor)).unwrap();
756            let mut opposite = valid.clone();
757            opposite[12..14]
758                .copy_from_slice(&(if successor { 1_u16 } else { 2_u16 }).to_be_bytes());
759            assert!(decode(&opposite).is_err());
760            assert_eq!(
761                crate::liteinst_initial_image::decode(&valid),
762                Err(crate::liteinst_initial_image::PacketError::Magic)
763            );
764        }
765        let mut missing_composite = encode(packet(true)).unwrap();
766        missing_composite.drain(192..224);
767        advertised(&mut missing_composite);
768        assert!(decode(&missing_composite).is_err());
769        let mut spurious_composite = encode(packet(false)).unwrap();
770        spurious_composite.splice(192..192, [7; 32]);
771        advertised(&mut spurious_composite);
772        assert!(decode(&spurious_composite).is_err());
773        let path = vec![b'x'; 200];
774        let legacy = crate::liteinst_initial_image::Packet {
775            nonce: &[1; 32],
776            config_digest: &[2; 32],
777            schema_fingerprint: b"0123456789abcdef",
778            coordinator: &path,
779            detlog_identity: "19:7:1234",
780            forward_policy: "INFO",
781        };
782        let legacy_bytes = crate::liteinst_initial_image::encode(legacy).unwrap();
783        assert_eq!(
784            crate::liteinst_initial_image::decode(&legacy_bytes),
785            Ok(legacy)
786        );
787        assert_eq!(decode(&legacy_bytes), Err(PacketError::Magic));
788    }
789
790    #[test]
791    fn packet_tokens_and_identity_text_are_data_not_socket_or_redemption_proof() {
792        let mut original = packet(false);
793        original.image = Image::Initial { nonce: &[0; 32] };
794        // Shape does not invent real descriptor separation or one-use proof.
795        original.observer_identity = original.detlog_identity;
796        let bytes = encode(original).unwrap();
797        assert_eq!(decode(&bytes), Ok(original));
798        let mut successor = packet(true);
799        successor.image = Image::Successor {
800            ticket: &[0; 32],
801            composite_digest: &[0; 32],
802        };
803        let bytes = encode(successor).unwrap();
804        assert_eq!(decode(&bytes), Ok(successor));
805        assert!(!format!("{:?}", original.image).contains("[0,"));
806        assert!(!format!("{:?}", successor.image).contains("ticket: ["));
807    }
808    #[test]
809    fn preallocated_encode_preserves_every_existing_wire_byte_and_unused_tail() {
810        for successor in [false, true] {
811            let value = packet(successor);
812            let expected = encode(value).unwrap();
813            let mut output = [0xa5; MAX_PACKET_LEN];
814            let length = encode_into(value, &mut output).unwrap();
815            assert_eq!(&output[..length], expected);
816            assert!(output[length..].iter().all(|byte| *byte == 0xa5));
817            assert_eq!(decode(&output[..length]).unwrap(), value);
818            for short in 0..length {
819                let mut output = vec![0xa5; short];
820                assert_eq!(encode_into(value, &mut output), Err(PacketError::Size));
821                assert!(output.iter().all(|byte| *byte == 0xa5));
822            }
823        }
824    }
825
826    #[test]
827    fn preallocated_invalid_fields_fail_before_writing_any_output() {
828        for successor in [false, true] {
829            for mode in 0..5 {
830                let mut value = packet(successor);
831                match mode {
832                    0 => value.coordinator = b"",
833                    1 => value.coordinator = b"bad\0path",
834                    2 => value.config_schema = b"not-hexadecimal!",
835                    3 => value.forward_policy = "",
836                    4 => value.observer_identity = "bad\0identity",
837                    _ => unreachable!(),
838                }
839                let expected = encode(value).unwrap_err();
840                let mut output = [0xa5; MAX_PACKET_LEN];
841                assert_eq!(encode_into(value, &mut output), Err(expected));
842                assert!(output.iter().all(|byte| *byte == 0xa5));
843            }
844        }
845    }
846}