Skip to main content

pcf_debug/plugin/
pcfsig.rs

1//! Decoders for PCF-SIG records (see `specs/PCF-SIG-spec-v1.0.txt`):
2//! `PCFSIG_KEY` (partition type `0xAAAB0001`, magic `"PCFKEY\0\0"`) and
3//! `PCFSIG_SIG` (partition type `0xAAAB0002`, magic `"PCFSIG\0\0"`).
4//!
5//! Both decoders mirror the spec's byte tables field-for-field and report spec
6//! violations as warnings rather than failing.
7
8use pcf::HashAlgo;
9use pcf_sig::{
10    compute_fingerprint, is_crypto_hash, KeyFormat, SigAlgo, FINGERPRINT_SIZE, KEY_MAGIC,
11    KEY_PREFIX_SIZE, MANIFEST_PREFIX_SIZE, SIGNED_ENTRY_SIZE, SIG_MAGIC, TYPE_PCFSIG_KEY,
12    TYPE_PCFSIG_SIG,
13};
14
15use super::{
16    le_u16, le_u32, le_u64, uid_at, Decoded, FieldNode, FieldValue, PartitionDecoder, PartitionMeta,
17};
18
19/// Render an 8-byte magic field as ASCII (with embedded NULs shown as `\0`).
20fn magic8(b: &[u8]) -> String {
21    b.iter()
22        .map(|&c| {
23            if c == 0 {
24                "\\0".to_string()
25            } else if (0x20..0x7f).contains(&c) {
26                (c as char).to_string()
27            } else {
28                format!("\\x{c:02x}")
29            }
30        })
31        .collect()
32}
33
34fn sig_algo_name(id: u8) -> &'static str {
35    match SigAlgo::from_id(id) {
36        Ok(SigAlgo::Ed25519) => "Ed25519",
37        Ok(SigAlgo::RsaPssSha256) => "RSA-PSS-SHA-256",
38        Ok(SigAlgo::RsaPssSha512) => "RSA-PSS-SHA-512",
39        Ok(SigAlgo::RsaPkcs1v15Sha256) => "RSA-PKCS1v15-SHA-256",
40        Ok(SigAlgo::RsaPkcs1v15Sha512) => "RSA-PKCS1v15-SHA-512",
41        Ok(SigAlgo::EcdsaP256Sha256) => "ECDSA-P256-SHA-256",
42        Ok(SigAlgo::EcdsaP521Sha512) => "ECDSA-P521-SHA-512",
43        Ok(SigAlgo::X509Chain) => "X.509 chain",
44        Err(_) => "unknown",
45    }
46}
47
48fn key_format_name(id: u8) -> &'static str {
49    match KeyFormat::from_id(id) {
50        Ok(KeyFormat::Ed25519Raw) => "Ed25519 raw",
51        Ok(KeyFormat::RsaSpkiDer) => "RSA SPKI DER",
52        Ok(KeyFormat::EcdsaSpkiDer) => "ECDSA SPKI DER",
53        Ok(KeyFormat::X509Cert) => "X.509 certificate",
54        Ok(KeyFormat::X509Chain) => "X.509 certificate chain",
55        Err(_) => "unknown",
56    }
57}
58
59fn metadata_tag_name(tag: u16) -> &'static str {
60    match tag {
61        0x0000 => "reserved",
62        0x0001 => "subject_dn",
63        0x0002 => "not_before",
64        0x0003 => "not_after",
65        0x0004 => "issuer_dn",
66        0x0005 => "comment",
67        t if t >= 0x8000 => "application-private",
68        _ => "reserved (future)",
69    }
70}
71
72// ---------------------------------------------------------------------------
73// PCFSIG_KEY
74// ---------------------------------------------------------------------------
75
76pub struct PcfSigKeyDecoder;
77
78impl PartitionDecoder for PcfSigKeyDecoder {
79    fn name(&self) -> &'static str {
80        "pcfsig-key"
81    }
82
83    fn matches(&self, meta: &PartitionMeta, data: &[u8]) -> bool {
84        meta.partition_type == TYPE_PCFSIG_KEY || data.get(0..8) == Some(&KEY_MAGIC)
85    }
86
87    fn decode(&self, _meta: &PartitionMeta, data: &[u8]) -> Decoded {
88        let mut warnings = Vec::new();
89        let mut fields = Vec::new();
90
91        if data.len() < KEY_PREFIX_SIZE {
92            warnings.push(format!(
93                "record is {} bytes; PCFSIG_KEY needs at least a {KEY_PREFIX_SIZE}-byte prefix",
94                data.len()
95            ));
96        }
97
98        let magic_ok = data.get(0..8) == Some(&KEY_MAGIC);
99        if !magic_ok {
100            warnings.push("record_magic is not \"PCFKEY\\0\\0\"".into());
101        }
102        fields.push(
103            FieldNode::leaf(
104                "record_magic",
105                FieldValue::Text(magic8(data.get(0..8).unwrap_or(&[]))),
106                (0, 8),
107            )
108            .with_note(if magic_ok {
109                "magic OK"
110            } else {
111                "expected \"PCFKEY\\0\\0\""
112            }),
113        );
114
115        let version_major = le_u16(data, 8).unwrap_or(0);
116        let version_minor = le_u16(data, 10).unwrap_or(0);
117        if version_major != 1 {
118            warnings.push(format!(
119                "record_version_major is {version_major} (v1.0 reader expects 1)"
120            ));
121        }
122        fields.push(FieldNode::leaf(
123            "record_version_major",
124            FieldValue::U64(version_major as u64),
125            (8, 10),
126        ));
127        fields.push(FieldNode::leaf(
128            "record_version_minor",
129            FieldValue::U64(version_minor as u64),
130            (10, 12),
131        ));
132
133        let key_format_id = data.get(12).copied().unwrap_or(0);
134        if key_format_id == 0 {
135            warnings.push("key_format_id is 0 (reserved)".into());
136        } else if KeyFormat::from_id(key_format_id).is_err() {
137            warnings.push(format!("key_format_id {key_format_id} is unknown"));
138        }
139        fields.push(FieldNode::leaf(
140            "key_format_id",
141            FieldValue::Enum {
142                raw: key_format_id as u64,
143                name: key_format_name(key_format_id).into(),
144            },
145            (12, 13),
146        ));
147
148        let reserved = data.get(13..16).unwrap_or(&[]);
149        if reserved.iter().any(|&b| b != 0) {
150            warnings.push("reserved bytes (offset 13..16) must be 0".into());
151        }
152        fields.push(FieldNode::leaf(
153            "reserved",
154            FieldValue::Bytes(reserved.to_vec()),
155            (13, 16),
156        ));
157
158        let fingerprint_stored = data.get(16..16 + FINGERPRINT_SIZE).unwrap_or(&[]);
159        fields.push(FieldNode::leaf(
160            "fingerprint",
161            FieldValue::Bytes(fingerprint_stored.to_vec()),
162            (16, 16 + FINGERPRINT_SIZE as u64),
163        ));
164
165        let key_data_length = le_u32(data, 48).unwrap_or(0) as usize;
166        if key_data_length == 0 {
167            warnings.push("key_data_length is 0".into());
168        }
169        fields.push(FieldNode::leaf(
170            "key_data_length",
171            FieldValue::U64(key_data_length as u64),
172            (48, 52),
173        ));
174
175        let key_end = KEY_PREFIX_SIZE.saturating_add(key_data_length);
176        if key_end > data.len() {
177            warnings.push(format!(
178                "key_data runs past end of record ({key_end} > {})",
179                data.len()
180            ));
181        }
182        let key_data = data
183            .get(KEY_PREFIX_SIZE..key_end.min(data.len()))
184            .unwrap_or(&[]);
185        fields.push(FieldNode::leaf(
186            "key_data",
187            FieldValue::Bytes(key_data.to_vec()),
188            (KEY_PREFIX_SIZE as u64, key_end as u64),
189        ));
190
191        // Cross-check: recompute SHA-256(key_data) and compare to stored fingerprint.
192        if !key_data.is_empty() && fingerprint_stored.len() == FINGERPRINT_SIZE {
193            let recomputed = compute_fingerprint(key_data);
194            if recomputed.as_slice() != fingerprint_stored {
195                warnings.push(
196                    "stored fingerprint does not equal SHA-256(key_data) (spec Section 6.3)".into(),
197                );
198            }
199        }
200
201        // Optional metadata TLV stream.
202        if key_end < data.len() {
203            let mut tlv_group = FieldNode::group("optional_metadata");
204            let mut cur = key_end;
205            let mut entry_idx = 0usize;
206            while cur < data.len() {
207                if data.len() - cur < 6 {
208                    warnings.push(format!(
209                        "metadata TLV entry {entry_idx} is truncated ({} bytes left)",
210                        data.len() - cur
211                    ));
212                    break;
213                }
214                let tag = le_u16(data, cur).unwrap_or(0);
215                let len = le_u32(data, cur + 2).unwrap_or(0) as usize;
216                let value_start = cur + 6;
217                let value_end = value_start.saturating_add(len);
218                let mut entry = FieldNode::group(format!("entry[{entry_idx}]"));
219                entry.push(FieldNode::leaf(
220                    "tag",
221                    FieldValue::Enum {
222                        raw: tag as u64,
223                        name: metadata_tag_name(tag).into(),
224                    },
225                    (cur as u64, cur as u64 + 2),
226                ));
227                entry.push(FieldNode::leaf(
228                    "length",
229                    FieldValue::U64(len as u64),
230                    (cur as u64 + 2, cur as u64 + 6),
231                ));
232                if value_end > data.len() {
233                    warnings.push(format!(
234                        "metadata TLV entry {entry_idx} value ({len} bytes) runs past end of record"
235                    ));
236                    entry.push(FieldNode::leaf(
237                        "value",
238                        FieldValue::Bytes(data.get(value_start..).unwrap_or(&[]).to_vec()),
239                        (value_start as u64, data.len() as u64),
240                    ));
241                    tlv_group.push(entry);
242                    break;
243                }
244                let value = &data[value_start..value_end];
245                entry.push(FieldNode::leaf(
246                    "value",
247                    FieldValue::Bytes(value.to_vec()),
248                    (value_start as u64, value_end as u64),
249                ));
250                tlv_group.push(entry);
251                cur = value_end;
252                entry_idx += 1;
253            }
254            if entry_idx > 0 {
255                fields.push(tlv_group);
256            }
257        }
258
259        Decoded {
260            format_name: "PCFSIG_KEY".into(),
261            fields,
262            warnings,
263        }
264    }
265}
266
267// ---------------------------------------------------------------------------
268// PCFSIG_SIG
269// ---------------------------------------------------------------------------
270
271pub struct PcfSigSignatureDecoder;
272
273impl PartitionDecoder for PcfSigSignatureDecoder {
274    fn name(&self) -> &'static str {
275        "pcfsig-sig"
276    }
277
278    fn matches(&self, meta: &PartitionMeta, data: &[u8]) -> bool {
279        meta.partition_type == TYPE_PCFSIG_SIG || data.get(0..8) == Some(&SIG_MAGIC)
280    }
281
282    fn decode(&self, _meta: &PartitionMeta, data: &[u8]) -> Decoded {
283        let mut warnings = Vec::new();
284        let mut fields = Vec::new();
285
286        if data.len() < MANIFEST_PREFIX_SIZE {
287            warnings.push(format!(
288                "record is {} bytes; PCFSIG_SIG manifest needs at least {MANIFEST_PREFIX_SIZE}",
289                data.len()
290            ));
291        }
292
293        // ---- manifest prefix --------------------------------------------------
294        let mut manifest = FieldNode::group("manifest");
295
296        let magic_ok = data.get(0..8) == Some(&SIG_MAGIC);
297        if !magic_ok {
298            warnings.push("manifest_magic is not \"PCFSIG\\0\\0\"".into());
299        }
300        manifest.push(
301            FieldNode::leaf(
302                "manifest_magic",
303                FieldValue::Text(magic8(data.get(0..8).unwrap_or(&[]))),
304                (0, 8),
305            )
306            .with_note(if magic_ok {
307                "magic OK"
308            } else {
309                "expected \"PCFSIG\\0\\0\""
310            }),
311        );
312
313        let version_major = le_u16(data, 8).unwrap_or(0);
314        let version_minor = le_u16(data, 10).unwrap_or(0);
315        if version_major != 1 {
316            warnings.push(format!(
317                "manifest_version_major is {version_major} (v1.0 reader expects 1)"
318            ));
319        }
320        manifest.push(FieldNode::leaf(
321            "manifest_version_major",
322            FieldValue::U64(version_major as u64),
323            (8, 10),
324        ));
325        manifest.push(FieldNode::leaf(
326            "manifest_version_minor",
327            FieldValue::U64(version_minor as u64),
328            (10, 12),
329        ));
330
331        let sig_algo_id = data.get(12).copied().unwrap_or(0);
332        if sig_algo_id == 0 {
333            warnings.push("sig_algo_id is 0 (reserved)".into());
334        } else if SigAlgo::from_id(sig_algo_id).is_err() {
335            warnings.push(format!("sig_algo_id {sig_algo_id} is unknown"));
336        }
337        manifest.push(FieldNode::leaf(
338            "sig_algo_id",
339            FieldValue::Enum {
340                raw: sig_algo_id as u64,
341                name: sig_algo_name(sig_algo_id).into(),
342            },
343            (12, 13),
344        ));
345
346        let manifest_hash_id = data.get(13).copied().unwrap_or(0);
347        let (manifest_hash_name, hash_is_crypto) = match HashAlgo::from_id(manifest_hash_id) {
348            Ok(a) => (crate::model::algo_name(a), is_crypto_hash(a)),
349            Err(_) => ("unknown", false),
350        };
351        if !hash_is_crypto {
352            warnings.push(format!(
353                "manifest_hash_algo_id {manifest_hash_id} is not cryptographic (spec Section 9)"
354            ));
355        }
356        manifest.push(FieldNode::leaf(
357            "manifest_hash_algo_id",
358            FieldValue::Enum {
359                raw: manifest_hash_id as u64,
360                name: manifest_hash_name.into(),
361            },
362            (13, 14),
363        ));
364
365        let flags = le_u16(data, 14).unwrap_or(0);
366        if flags != 0 {
367            warnings.push(format!("flags is {flags:#06x}; v1.0 readers require 0"));
368        }
369        manifest.push(FieldNode::leaf(
370            "flags",
371            FieldValue::U64(flags as u64),
372            (14, 16),
373        ));
374
375        let signer_fp = data.get(16..16 + FINGERPRINT_SIZE).unwrap_or(&[]);
376        manifest.push(FieldNode::leaf(
377            "signer_key_fingerprint",
378            FieldValue::Bytes(signer_fp.to_vec()),
379            (16, 16 + FINGERPRINT_SIZE as u64),
380        ));
381
382        let signed_at = le_u64(data, 48).unwrap_or(0);
383        manifest.push(FieldNode::leaf(
384            "signed_at_unix_seconds",
385            FieldValue::U64(signed_at),
386            (48, 56),
387        ));
388
389        let signed_count = le_u32(data, 56).unwrap_or(0) as usize;
390        if signed_count == 0 {
391            warnings.push("signed_count is 0 (manifest must have at least 1 entry)".into());
392        }
393        manifest.push(FieldNode::leaf(
394            "signed_count",
395            FieldValue::U64(signed_count as u64),
396            (56, 60),
397        ));
398
399        // ---- signed_entries[] -------------------------------------------------
400        let mut entries_group = FieldNode::group("signed_entries");
401        for i in 0..signed_count {
402            let off = MANIFEST_PREFIX_SIZE + i * SIGNED_ENTRY_SIZE;
403            if off + SIGNED_ENTRY_SIZE > data.len() {
404                warnings.push(format!(
405                    "signed_entry[{i}] runs past end of record (offset {off}, len {})",
406                    data.len()
407                ));
408                break;
409            }
410            let mut entry = FieldNode::group(format!("entry[{i}]"));
411
412            let uid = uid_at(data, off).unwrap_or([0; 16]);
413            if uid == [0u8; 16] {
414                warnings.push(format!("signed_entry[{i}].uid is NIL"));
415            }
416            entry.push(FieldNode::leaf(
417                "uid",
418                FieldValue::Uid(uid),
419                (off as u64, off as u64 + 16),
420            ));
421
422            let ptype = le_u32(data, off + 16).unwrap_or(0);
423            if ptype == 0 {
424                warnings.push(format!("signed_entry[{i}].partition_type is 0 (reserved)"));
425            }
426            entry.push(FieldNode::leaf(
427                "partition_type",
428                FieldValue::U64(ptype as u64),
429                (off as u64 + 16, off as u64 + 20),
430            ));
431
432            let label_bytes = data.get(off + 20..off + 52).unwrap_or(&[]);
433            let label_end = label_bytes.iter().position(|&b| b == 0).unwrap_or(32);
434            let label_str =
435                String::from_utf8_lossy(&label_bytes[..label_end.min(label_bytes.len())])
436                    .into_owned();
437            entry.push(FieldNode::leaf(
438                "label",
439                FieldValue::Text(label_str),
440                (off as u64 + 20, off as u64 + 52),
441            ));
442
443            let used_bytes = le_u64(data, off + 52).unwrap_or(0);
444            entry.push(FieldNode::leaf(
445                "used_bytes",
446                FieldValue::U64(used_bytes),
447                (off as u64 + 52, off as u64 + 60),
448            ));
449
450            let entry_hash_id = data.get(off + 60).copied().unwrap_or(0);
451            let (entry_hash_name, entry_is_crypto) = match HashAlgo::from_id(entry_hash_id) {
452                Ok(a) => (crate::model::algo_name(a), is_crypto_hash(a)),
453                Err(_) => ("unknown", false),
454            };
455            if !entry_is_crypto {
456                warnings.push(format!(
457                    "signed_entry[{i}].data_hash_algo_id {entry_hash_id} is not cryptographic"
458                ));
459            }
460            entry.push(FieldNode::leaf(
461                "data_hash_algo_id",
462                FieldValue::Enum {
463                    raw: entry_hash_id as u64,
464                    name: entry_hash_name.into(),
465                },
466                (off as u64 + 60, off as u64 + 61),
467            ));
468
469            let reserved1 = data.get(off + 61).copied().unwrap_or(0);
470            if reserved1 != 0 {
471                warnings.push(format!(
472                    "signed_entry[{i}] reserved byte at offset 61 is {reserved1:#04x} (must be 0)"
473                ));
474            }
475            entry.push(FieldNode::leaf(
476                "reserved (1 B)",
477                FieldValue::U64(reserved1 as u64),
478                (off as u64 + 61, off as u64 + 62),
479            ));
480
481            let data_hash = data.get(off + 62..off + 126).unwrap_or(&[]);
482            entry.push(FieldNode::leaf(
483                "data_hash",
484                FieldValue::Bytes(data_hash.to_vec()),
485                (off as u64 + 62, off as u64 + 126),
486            ));
487
488            let reserved2 = data.get(off + 126..off + 218).unwrap_or(&[]);
489            if reserved2.iter().any(|&b| b != 0) {
490                warnings.push(format!(
491                    "signed_entry[{i}] reserved tail (92 B at offset 126) must be all-zero"
492                ));
493            }
494            entry.push(FieldNode::leaf(
495                "reserved (92 B)",
496                FieldValue::Bytes(reserved2.to_vec()),
497                (off as u64 + 126, off as u64 + 218),
498            ));
499
500            entries_group.push(entry);
501        }
502        manifest.push(entries_group);
503        fields.push(manifest);
504
505        // ---- tail: sig_length || sig_bytes || trailer_length -----------------
506        let manifest_len = MANIFEST_PREFIX_SIZE + signed_count * SIGNED_ENTRY_SIZE;
507        if data.len() >= manifest_len + 4 {
508            let sig_length = le_u32(data, manifest_len).unwrap_or(0) as usize;
509            fields.push(FieldNode::leaf(
510                "sig_length",
511                FieldValue::U64(sig_length as u64),
512                (manifest_len as u64, manifest_len as u64 + 4),
513            ));
514
515            let sig_start = manifest_len + 4;
516            let sig_end = sig_start.saturating_add(sig_length);
517            if sig_end > data.len() {
518                warnings.push(format!(
519                    "sig_bytes ({sig_length} bytes) runs past end of record"
520                ));
521            }
522            let sig_bytes = data.get(sig_start..sig_end.min(data.len())).unwrap_or(&[]);
523            fields.push(FieldNode::leaf(
524                "sig_bytes",
525                FieldValue::Bytes(sig_bytes.to_vec()),
526                (sig_start as u64, sig_end as u64),
527            ));
528
529            if data.len() >= sig_end + 4 {
530                let trailer_length = le_u32(data, sig_end).unwrap_or(0) as usize;
531                if trailer_length != 0 {
532                    warnings.push(format!(
533                        "trailer_length is {trailer_length}; v1.0 readers require 0"
534                    ));
535                }
536                fields.push(FieldNode::leaf(
537                    "trailer_length",
538                    FieldValue::U64(trailer_length as u64),
539                    (sig_end as u64, sig_end as u64 + 4),
540                ));
541                if trailer_length > 0 {
542                    let trailer_bytes = data
543                        .get(sig_end + 4..(sig_end + 4 + trailer_length).min(data.len()))
544                        .unwrap_or(&[]);
545                    fields.push(FieldNode::leaf(
546                        "trailer_bytes",
547                        FieldValue::Bytes(trailer_bytes.to_vec()),
548                        (sig_end as u64 + 4, (sig_end + 4 + trailer_length) as u64),
549                    ));
550                }
551            } else {
552                warnings.push("trailer_length field missing (record is truncated)".into());
553            }
554        } else {
555            warnings.push("sig_length field missing (record is truncated)".into());
556        }
557
558        Decoded {
559            format_name: "PCFSIG_SIG".into(),
560            fields,
561            warnings,
562        }
563    }
564}