1use matter_cert::MatterCertificate;
48use matter_codec::{Tag, TlvWriter, Value};
49
50use crate::error::Error;
51use crate::state::{
52 CommissionerIdentity, ControllerState, DeviceEntry, FabricEntry, GroupKeySetConfig,
53 IcacIdentity,
54};
55
56pub const SNAPSHOT_VERSION: u8 = 1;
58
59pub fn serialize(state: &ControllerState) -> Result<Vec<u8>, Error> {
66 let mut fabrics = Vec::with_capacity(state.fabrics.len());
67 for f in &state.fabrics {
68 fabrics.push(fabric_to_value(f)?);
69 }
70 let root = Value::Structure(vec![
71 (Tag::Context(0), Value::Uint(u64::from(SNAPSHOT_VERSION))),
72 (Tag::Context(1), Value::Array(fabrics)),
73 ]);
74
75 let mut out = Vec::new();
76 let mut w = TlvWriter::new(&mut out);
77 w.write_value(Tag::Anonymous, &root)?;
78 Ok(out)
79}
80
81fn fabric_to_value(f: &FabricEntry) -> Result<Value, Error> {
82 let devices = f.devices.iter().map(device_to_value).collect();
83 let group_keys: Vec<Value> = f.group_keys.iter().map(group_key_to_value).collect();
84 let icd_clients: Vec<Value> = f
85 .icd_clients
86 .iter()
87 .map(icd_registration_to_value)
88 .collect();
89 let mut members = vec![
90 (Tag::Context(0), Value::Uint(f.fabric_id)),
91 (Tag::Context(1), Value::Bytes(f.ipk.to_vec())),
92 (Tag::Context(2), Value::Bytes(f.rcac_cert.to_tlv()?)),
93 (Tag::Context(3), Value::Bytes(f.rcac_pkcs8.clone())),
94 (Tag::Context(4), commissioner_to_value(&f.commissioner)?),
95 (Tag::Context(5), Value::Array(devices)),
96 (Tag::Context(6), Value::Array(group_keys)),
97 (
98 Tag::Context(7),
99 Value::Uint(u64::from(f.outbound_group_counter)),
100 ),
101 (Tag::Context(8), Value::Array(icd_clients)),
102 ];
103 if let Some(icac) = &f.icac {
107 members.push((Tag::Context(9), Value::Bytes(icac.cert.to_tlv()?)));
108 members.push((Tag::Context(10), Value::Bytes(icac.pkcs8.clone())));
109 }
110 Ok(Value::Structure(members))
111}
112
113fn icd_registration_to_value(r: &crate::icd::IcdRegistration) -> Value {
114 Value::Structure(vec![
115 (Tag::Context(0), Value::Uint(r.node_id)),
116 (Tag::Context(1), Value::Uint(r.check_in_node_id)),
117 (Tag::Context(2), Value::Uint(r.monitored_subject)),
118 (Tag::Context(3), Value::Bytes(r.key.to_vec())),
119 (Tag::Context(4), Value::Uint(u64::from(r.start_counter))),
120 ])
121}
122
123fn group_key_to_value(k: &GroupKeySetConfig) -> Value {
124 Value::Structure(vec![
125 (Tag::Context(0), Value::Uint(u64::from(k.key_set_id))),
126 (Tag::Context(1), Value::Bytes(k.epoch_key.to_vec())),
127 (Tag::Context(2), Value::Uint(k.epoch_start_time)),
128 ])
129}
130
131fn commissioner_to_value(c: &CommissionerIdentity) -> Result<Value, Error> {
132 Ok(Value::Structure(vec![
133 (Tag::Context(0), Value::Uint(c.node_id)),
134 (Tag::Context(1), Value::Bytes(c.operational_pkcs8.clone())),
135 (Tag::Context(2), Value::Bytes(c.noc.to_tlv()?)),
136 ]))
137}
138
139fn device_to_value(d: &DeviceEntry) -> Value {
140 let mut members = vec![
141 (Tag::Context(0), Value::Uint(d.node_id)),
142 (
143 Tag::Context(1),
144 Value::Bytes(d.peer_noc_public_key.to_vec()),
145 ),
146 ];
147 if let Some(rr) = &d.resumption_record {
148 members.push((Tag::Context(2), Value::Bytes(rr.clone())));
149 }
150 if let Some(addr) = &d.last_known_addr {
151 members.push((Tag::Context(3), Value::Utf8(addr.clone())));
152 }
153 if let Some(vid) = d.vendor_id {
154 members.push((Tag::Context(4), Value::Uint(u64::from(vid))));
155 }
156 if let Some(pid) = d.product_id {
157 members.push((Tag::Context(5), Value::Uint(u64::from(pid))));
158 }
159 if let Some(label) = &d.label {
160 members.push((Tag::Context(6), Value::Utf8(label.clone())));
161 }
162 Value::Structure(members)
163}
164
165pub fn deserialize(bytes: &[u8]) -> Result<ControllerState, Error> {
173 use matter_codec::TlvReader;
174
175 let mut r = TlvReader::new(bytes);
176 let (_tag, value) = r.read_value()?;
177 let root = as_struct(&value)?;
178
179 let version = get_uint(root, 0)?;
180 if version != u64::from(SNAPSHOT_VERSION) {
181 return Err(Error::Snapshot(format!(
182 "unsupported snapshot version {version}"
183 )));
184 }
185
186 let fabrics_val =
187 get(root, 1).ok_or_else(|| Error::Snapshot("missing fabrics array".into()))?;
188 let mut fabrics = Vec::new();
189 for fv in as_array(fabrics_val)? {
190 fabrics.push(fabric_from_value(fv)?);
191 }
192 Ok(ControllerState { fabrics })
193}
194
195fn fabric_from_value(v: &Value) -> Result<FabricEntry, Error> {
196 let m = as_struct(v)?;
197 let commissioner_val =
198 get(m, 4).ok_or_else(|| Error::Snapshot("missing commissioner".into()))?;
199 let devices_val = get(m, 5).ok_or_else(|| Error::Snapshot("missing devices array".into()))?;
200 let mut devices = Vec::new();
201 for dv in as_array(devices_val)? {
202 devices.push(device_from_value(dv)?);
203 }
204
205 let group_keys = match get(m, 6) {
209 Some(arr) => {
210 let mut keys = Vec::new();
211 for kv in as_array(arr)? {
212 keys.push(group_key_from_value(kv)?);
213 }
214 keys
215 }
216 None => Vec::new(),
217 };
218 let outbound_group_counter = match get(m, 7) {
219 Some(Value::Uint(n)) => u32::try_from(*n)
220 .map_err(|_| Error::Snapshot("outbound_group_counter exceeds u32 range".into()))?,
221 _ => 0,
222 };
223 let icd_clients = match get(m, 8) {
226 Some(arr) => {
227 let mut regs = Vec::new();
228 for rv in as_array(arr)? {
229 regs.push(icd_registration_from_value(rv)?);
230 }
231 regs
232 }
233 None => Vec::new(),
234 };
235
236 let icac = match (get(m, 9), get(m, 10)) {
243 (Some(Value::Bytes(cert_tlv)), Some(Value::Bytes(pkcs8))) => Some(IcacIdentity {
244 cert: MatterCertificate::from_tlv(cert_tlv)?,
245 pkcs8: pkcs8.clone(),
246 }),
247 _ => None,
248 };
249
250 Ok(FabricEntry {
251 fabric_id: get_uint(m, 0)?,
252 ipk: byte_array::<16>(get_bytes(m, 1)?, "ipk")?,
253 rcac_cert: MatterCertificate::from_tlv(get_bytes(m, 2)?)?,
254 rcac_pkcs8: get_bytes(m, 3)?.to_vec(),
255 commissioner: commissioner_from_value(commissioner_val)?,
256 devices,
257 group_keys,
258 outbound_group_counter,
259 icd_clients,
260 icac,
261 })
262}
263
264fn icd_registration_from_value(v: &Value) -> Result<crate::icd::IcdRegistration, Error> {
265 let m = as_struct(v)?;
266 let start_counter = u32::try_from(get_uint(m, 4)?)
267 .map_err(|_| Error::Snapshot("icd start_counter exceeds u32 range".into()))?;
268 Ok(crate::icd::IcdRegistration::new(
269 get_uint(m, 0)?,
270 get_uint(m, 1)?,
271 get_uint(m, 2)?,
272 byte_array::<16>(get_bytes(m, 3)?, "icd key")?,
273 start_counter,
274 ))
275}
276
277fn group_key_from_value(v: &Value) -> Result<GroupKeySetConfig, Error> {
278 let m = as_struct(v)?;
279 let key_set_id = u16::try_from(get_uint(m, 0)?)
280 .map_err(|_| Error::Snapshot("key_set_id exceeds u16 range".into()))?;
281 let epoch_key = byte_array::<16>(get_bytes(m, 1)?, "epoch_key")?;
282 let epoch_start_time = get_uint(m, 2)?;
283 Ok(GroupKeySetConfig::new(
284 key_set_id,
285 epoch_key,
286 epoch_start_time,
287 ))
288}
289
290fn commissioner_from_value(v: &Value) -> Result<CommissionerIdentity, Error> {
291 let m = as_struct(v)?;
292 Ok(CommissionerIdentity {
293 node_id: get_uint(m, 0)?,
294 operational_pkcs8: get_bytes(m, 1)?.to_vec(),
295 noc: MatterCertificate::from_tlv(get_bytes(m, 2)?)?,
296 })
297}
298
299fn device_from_value(v: &Value) -> Result<DeviceEntry, Error> {
300 let m = as_struct(v)?;
301 let resumption_record = match get(m, 2) {
302 Some(Value::Bytes(b)) => Some(b.clone()),
303 _ => None,
304 };
305 let last_known_addr = match get(m, 3) {
306 Some(Value::Utf8(s)) => Some(s.clone()),
307 _ => None,
308 };
309 let vendor_id = match get(m, 4) {
315 Some(Value::Uint(n)) => u16::try_from(*n).ok(),
316 _ => None,
317 };
318 let product_id = match get(m, 5) {
319 Some(Value::Uint(n)) => u16::try_from(*n).ok(),
320 _ => None,
321 };
322 let label = match get(m, 6) {
323 Some(Value::Utf8(s)) => Some(s.clone()),
324 _ => None,
325 };
326 Ok(DeviceEntry {
327 node_id: get_uint(m, 0)?,
328 peer_noc_public_key: byte_array::<65>(get_bytes(m, 1)?, "peer_noc_public_key")?,
329 resumption_record,
330 last_known_addr,
331 vendor_id,
332 product_id,
333 label,
334 })
335}
336
337fn as_struct(v: &Value) -> Result<&[(Tag, Value)], Error> {
340 match v {
341 Value::Structure(members) => Ok(members),
342 _ => Err(Error::Snapshot("expected structure".into())),
343 }
344}
345
346fn as_array(v: &Value) -> Result<&[Value], Error> {
347 match v {
348 Value::Array(items) => Ok(items),
349 _ => Err(Error::Snapshot("expected array".into())),
350 }
351}
352
353fn get(members: &[(Tag, Value)], ctx: u8) -> Option<&Value> {
354 members
355 .iter()
356 .find(|(t, _)| *t == Tag::Context(ctx))
357 .map(|(_, v)| v)
358}
359
360fn get_uint(members: &[(Tag, Value)], ctx: u8) -> Result<u64, Error> {
361 match get(members, ctx) {
362 Some(Value::Uint(n)) => Ok(*n),
363 _ => Err(Error::Snapshot(format!(
364 "missing or non-uint at context {ctx}"
365 ))),
366 }
367}
368
369fn get_bytes(members: &[(Tag, Value)], ctx: u8) -> Result<&[u8], Error> {
370 match get(members, ctx) {
371 Some(Value::Bytes(b)) => Ok(b),
372 _ => Err(Error::Snapshot(format!(
373 "missing or non-bytes at context {ctx}"
374 ))),
375 }
376}
377
378fn byte_array<const N: usize>(b: &[u8], field: &str) -> Result<[u8; N], Error> {
379 b.try_into()
380 .map_err(|_| Error::Snapshot(format!("{field}: expected {N} bytes, got {}", b.len())))
381}
382
383#[cfg(test)]
384#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
386 use super::*;
387 use crate::fabric::{create_fabric, FabricConfig};
388 use matter_cert::MatterTime;
389 use matter_commissioning::SystemNocRng;
390
391 fn sample_state() -> ControllerState {
392 let cfg = FabricConfig {
393 fabric_id: 0x1122_3344_5566_7788,
394 rcac_id: 1,
395 commissioner_node_id: 0x0000_0000_0000_0001,
396 validity: (
397 MatterTime::from_unix_secs(1_700_000_000),
398 MatterTime::NO_EXPIRY,
399 ),
400 issue_icac: false,
401 };
402 let mut fabric = create_fabric(&cfg, &SystemNocRng).expect("create_fabric");
403 fabric.devices.push(DeviceEntry {
404 node_id: 0xABCD,
405 peer_noc_public_key: [0x04; 65],
406 resumption_record: Some(vec![1, 2, 3, 4]),
407 last_known_addr: Some("[fe80::1]:5540".to_string()),
408 vendor_id: None,
409 product_id: None,
410 label: None,
411 });
412 fabric.devices.push(DeviceEntry {
413 node_id: 0xBEEF,
414 peer_noc_public_key: [0x04; 65],
415 resumption_record: None,
416 last_known_addr: None,
417 vendor_id: None,
418 product_id: None,
419 label: None,
420 });
421 ControllerState {
422 fabrics: vec![fabric],
423 }
424 }
425
426 #[test]
427 fn round_trips_a_full_state() {
428 let state = sample_state();
429 let bytes = serialize(&state).expect("serialize");
430 let back = deserialize(&bytes).expect("deserialize");
431
432 assert_eq!(back.fabrics.len(), 1);
433 let (a, b) = (&state.fabrics[0], &back.fabrics[0]);
434 assert_eq!(a.fabric_id, b.fabric_id);
435 assert_eq!(a.ipk, b.ipk);
436 assert_eq!(a.rcac_pkcs8, b.rcac_pkcs8);
437 assert_eq!(a.rcac_cert.to_tlv().unwrap(), b.rcac_cert.to_tlv().unwrap());
438 assert_eq!(a.commissioner.node_id, b.commissioner.node_id);
439 assert_eq!(
440 a.commissioner.operational_pkcs8,
441 b.commissioner.operational_pkcs8
442 );
443 assert_eq!(
444 a.commissioner.noc.to_tlv().unwrap(),
445 b.commissioner.noc.to_tlv().unwrap()
446 );
447 assert_eq!(a.devices.len(), b.devices.len());
448 assert_eq!(a.devices[0].node_id, b.devices[0].node_id);
449 assert_eq!(
450 a.devices[0].resumption_record,
451 b.devices[0].resumption_record
452 );
453 assert_eq!(a.devices[0].last_known_addr, b.devices[0].last_known_addr);
454 assert_eq!(a.devices[1].resumption_record, None);
455 assert_eq!(a.devices[1].last_known_addr, None);
456 }
457
458 #[test]
459 fn empty_state_round_trips() {
460 let bytes = serialize(&ControllerState::default()).expect("serialize");
461 assert!(deserialize(&bytes).expect("deserialize").fabrics.is_empty());
462 }
463
464 #[test]
465 fn rejects_unknown_version() {
466 let root = Value::Structure(vec![
468 (Tag::Context(0), Value::Uint(99)),
469 (Tag::Context(1), Value::Array(vec![])),
470 ]);
471 let mut out = Vec::new();
472 let mut w = TlvWriter::new(&mut out);
473 w.write_value(Tag::Anonymous, &root).unwrap();
474 let err = deserialize(&out).expect_err("must reject");
475 assert!(matches!(err, Error::Snapshot(_)));
476 }
477
478 use proptest::prelude::*;
481 use std::sync::OnceLock;
482
483 fn shared_fabric() -> &'static FabricEntry {
486 static FABRIC: OnceLock<FabricEntry> = OnceLock::new();
487 FABRIC.get_or_init(|| {
488 let cfg = FabricConfig {
489 fabric_id: 0x0102_0304_0506_0708,
490 rcac_id: 1,
491 commissioner_node_id: 0x0000_0000_0000_0001,
492 validity: (
493 MatterTime::from_unix_secs(1_700_000_000),
494 MatterTime::NO_EXPIRY,
495 ),
496 issue_icac: false,
497 };
498 create_fabric(&cfg, &SystemNocRng).expect("mint shared fabric")
499 })
500 }
501
502 prop_compose! {
503 fn arb_device()(
504 node_id in any::<u64>(),
505 pk in prop::collection::vec(any::<u8>(), 65),
506 rr in prop::option::of(prop::collection::vec(any::<u8>(), 0..40)),
507 addr in prop::option::of("[ -~]{0,32}"),
508 ) -> DeviceEntry {
509 let mut peer_noc_public_key = [0u8; 65];
510 peer_noc_public_key.copy_from_slice(&pk);
511 DeviceEntry {
512 node_id,
513 peer_noc_public_key,
514 resumption_record: rr,
515 last_known_addr: addr,
516 vendor_id: None,
517 product_id: None,
518 label: None,
519 }
520 }
521 }
522
523 proptest! {
524 #[test]
526 fn snapshot_round_trips(devices in prop::collection::vec(arb_device(), 0..6)) {
527 let mut fabric = shared_fabric().clone();
528 fabric.devices = devices.clone();
529 let state = ControllerState { fabrics: vec![fabric] };
530
531 let bytes = serialize(&state).expect("serialize");
532 let back = deserialize(&bytes).expect("deserialize");
533
534 prop_assert_eq!(back.fabrics.len(), 1);
535 let dev_back = &back.fabrics[0].devices;
536 prop_assert_eq!(dev_back.len(), devices.len());
537 for (a, b) in devices.iter().zip(dev_back.iter()) {
538 prop_assert_eq!(a.node_id, b.node_id);
539 prop_assert_eq!(a.peer_noc_public_key, b.peer_noc_public_key);
540 prop_assert_eq!(&a.resumption_record, &b.resumption_record);
541 prop_assert_eq!(&a.last_known_addr, &b.last_known_addr);
542 }
543 }
544 }
545
546 #[test]
549 fn group_keys_round_trip() {
550 let mut fabric = shared_fabric().clone();
553 fabric.group_keys = vec![
554 GroupKeySetConfig::new(0x0001, [0xAA; 16], 1_700_000_000),
555 GroupKeySetConfig::new(0x0002, [0xBB; 16], 1_700_100_000),
556 ];
557 fabric.outbound_group_counter = 42;
558 let state = ControllerState {
559 fabrics: vec![fabric.clone()],
560 };
561
562 let bytes = serialize(&state).expect("serialize");
563 let back = deserialize(&bytes).expect("deserialize");
564
565 assert_eq!(back.fabrics.len(), 1);
566 let f = &back.fabrics[0];
567 assert_eq!(f.outbound_group_counter, 42);
568 assert_eq!(f.group_keys.len(), 2);
569 assert_eq!(f.group_keys[0].key_set_id, 0x0001);
570 assert_eq!(f.group_keys[0].epoch_key, [0xAA; 16]);
571 assert_eq!(f.group_keys[0].epoch_start_time, 1_700_000_000);
572 assert_eq!(f.group_keys[1].key_set_id, 0x0002);
573 assert_eq!(f.group_keys[1].epoch_key, [0xBB; 16]);
574 assert_eq!(f.group_keys[1].epoch_start_time, 1_700_100_000);
575 }
576
577 #[test]
578 fn icd_clients_round_trip() {
579 let mut fabric = shared_fabric().clone();
582 fabric.icd_clients = vec![
583 crate::icd::IcdRegistration::new(0x0042, 1, 1, [0xCC; 16], 7),
584 crate::icd::IcdRegistration::new(0x0043, 1, 2, [0xDD; 16], 99),
585 ];
586 let state = ControllerState {
587 fabrics: vec![fabric.clone()],
588 };
589 let bytes = serialize(&state).expect("serialize");
590 let back = deserialize(&bytes).expect("deserialize");
591 assert_eq!(back.fabrics[0].icd_clients, fabric.icd_clients);
592 }
593
594 fn sample_icac(fabric: &FabricEntry) -> crate::state::IcacIdentity {
599 use matter_cert::operational::{icac, sign_with_ring, IcacParams};
600 use matter_cert::PublicKey;
601 use matter_crypto::{RingSigner, Signer};
602
603 let (icac_signer, icac_pkcs8) = RingSigner::generate().expect("generate icac key");
604 let icac_public_key =
605 PublicKey::new(*icac_signer.public_key().as_bytes()).expect("valid P-256 public key");
606 let issuer_skid = fabric
607 .rcac_cert
608 .extensions()
609 .subject_key_identifier
610 .expect("rcac has SKID");
611
612 let unsigned = icac(IcacParams::new(
613 0x0000_0000_0000_0099,
614 fabric.rcac_cert.subject().clone(),
615 issuer_skid,
616 icac_public_key,
617 vec![0x01],
618 MatterTime::from_unix_secs(1_700_000_000),
619 MatterTime::NO_EXPIRY,
620 ))
621 .expect("build unsigned icac");
622 let cert = sign_with_ring(unsigned, &fabric.rcac_pkcs8).expect("sign icac");
623
624 crate::state::IcacIdentity {
625 cert,
626 pkcs8: icac_pkcs8,
627 }
628 }
629
630 #[test]
631 fn snapshot_round_trips_fabric_with_icac() {
632 let mut fabric = shared_fabric().clone();
635 let icac_identity = sample_icac(&fabric);
636 fabric.icac = Some(icac_identity);
637
638 let state = ControllerState {
639 fabrics: vec![fabric.clone()],
640 };
641 let bytes = serialize(&state).expect("serialize");
642 let back = deserialize(&bytes).expect("deserialize");
643
644 let want = fabric.icac.as_ref().expect("icac set on input fabric");
645 let got = back.fabrics[0]
646 .icac
647 .as_ref()
648 .expect("icac must round-trip as Some");
649 assert_eq!(got.cert.to_tlv().unwrap(), want.cert.to_tlv().unwrap());
650 assert_eq!(got.pkcs8, want.pkcs8);
651 }
652
653 #[test]
654 fn snapshot_without_icac_is_backward_compatible() {
655 let fabric = shared_fabric().clone();
659 assert!(fabric.icac.is_none());
660 let state = ControllerState {
661 fabrics: vec![fabric],
662 };
663
664 let bytes = serialize(&state).expect("serialize");
665 let back = deserialize(&bytes).expect("deserialize");
666 assert!(back.fabrics[0].icac.is_none());
667
668 let mut r = matter_codec::TlvReader::new(&bytes);
674 let (_tag, root) = r.read_value().expect("read root");
675 let root_members = as_struct(&root).expect("root struct");
676 let fabrics_arr = get(root_members, 1).expect("fabrics array");
677 let first_fabric = &as_array(fabrics_arr).expect("fabrics array")[0];
678 let fabric_members = as_struct(first_fabric).expect("fabric struct");
679 assert!(
680 get(fabric_members, 9).is_none(),
681 "C9 (icac_cert) must be absent when icac is None"
682 );
683 assert!(
684 get(fabric_members, 10).is_none(),
685 "C10 (icac_pkcs8) must be absent when icac is None"
686 );
687 }
688
689 #[test]
692 fn device_metadata_round_trips_and_defaults_none() {
693 let mut fabric = shared_fabric().clone();
696 fabric.devices = vec![DeviceEntry {
697 node_id: 0x1234,
698 peer_noc_public_key: [0x04; 65],
699 resumption_record: None,
700 last_known_addr: None,
701 vendor_id: Some(0xFFF1),
702 product_id: Some(0x8000),
703 label: Some("kitchen plug".to_string()),
704 }];
705 let state = ControllerState {
706 fabrics: vec![fabric],
707 };
708 let bytes = serialize(&state).expect("serialize");
709 let back = deserialize(&bytes).expect("deserialize");
710 let d = &back.fabrics[0].devices[0];
711 assert_eq!(d.vendor_id, Some(0xFFF1));
712 assert_eq!(d.product_id, Some(0x8000));
713 assert_eq!(d.label.as_deref(), Some("kitchen plug"));
714
715 let old_device_val = Value::Structure(vec![
720 (Tag::Context(0), Value::Uint(0x9999)),
721 (Tag::Context(1), Value::Bytes(vec![0x04; 65])),
722 ]);
723 let old_device = device_from_value(&old_device_val).expect("old device must load");
724 assert_eq!(old_device.vendor_id, None);
725 assert_eq!(old_device.product_id, None);
726 assert_eq!(old_device.label, None);
727 }
728
729 #[test]
730 fn old_snapshot_without_t6_t7_loads_with_defaults() {
731 let fabric = shared_fabric().clone();
735
736 let old_fabric_val = Value::Structure(vec![
738 (Tag::Context(0), Value::Uint(fabric.fabric_id)),
739 (Tag::Context(1), Value::Bytes(fabric.ipk.to_vec())),
740 (
741 Tag::Context(2),
742 Value::Bytes(fabric.rcac_cert.to_tlv().expect("rcac tlv")),
743 ),
744 (Tag::Context(3), Value::Bytes(fabric.rcac_pkcs8.clone())),
745 (
746 Tag::Context(4),
747 commissioner_to_value(&fabric.commissioner).expect("commissioner"),
748 ),
749 (Tag::Context(5), Value::Array(vec![])), ]);
751
752 let root = Value::Structure(vec![
753 (Tag::Context(0), Value::Uint(u64::from(SNAPSHOT_VERSION))),
754 (Tag::Context(1), Value::Array(vec![old_fabric_val])),
755 ]);
756
757 let mut out = Vec::new();
758 let mut w = TlvWriter::new(&mut out);
759 w.write_value(Tag::Anonymous, &root).unwrap();
760
761 let back = deserialize(&out).expect("old snapshot must load without error");
762 assert_eq!(back.fabrics.len(), 1);
763 let f = &back.fabrics[0];
764 assert!(
765 f.group_keys.is_empty(),
766 "group_keys must default to empty for old snapshot"
767 );
768 assert_eq!(
769 f.outbound_group_counter, 0,
770 "outbound_group_counter must default to 0 for old snapshot"
771 );
772 }
773}