Skip to main content

prikk_object/payload/
attestation.rs

1//! Attestation payload types.
2
3use prikk_error::{PrikkError, Result};
4
5use crate::{CanonicalEncode, CanonicalWriter, ObjectId};
6
7/// Attestation status.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9#[repr(u16)]
10pub enum AttestationStatus {
11    /// Passed policy.
12    Pass = 1,
13    /// Warning.
14    Warn = 2,
15    /// Failed policy.
16    Fail = 3,
17    /// Locally quarantined.
18    Quarantine = 4,
19}
20
21impl AttestationStatus {
22    /// Stable code.
23    #[must_use]
24    pub const fn code(self) -> u16 {
25        self as u16
26    }
27}
28
29/// Plugin result entry, sorted by plugin ID. FDD-03 §9.10. The canonical sort key
30/// is `plugin_id` only (see `results_sorted_by_plugin_id`), so a full-record
31/// ordering is deliberately not derived.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct PluginResultEntry {
34    /// Plugin ID.
35    pub plugin_id: String,
36    /// Plugin version.
37    pub plugin_version: String,
38    /// Status.
39    pub status: AttestationStatus,
40    /// Report hash (not an object id).
41    pub report_hash: Vec<u8>,
42    /// Number of findings reported.
43    pub finding_count: u32,
44}
45
46impl CanonicalEncode for PluginResultEntry {
47    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
48        writer.field_string(1, &self.plugin_id)?;
49        writer.field_string(2, &self.plugin_version)?;
50        writer.field_enum_u16(3, self.status.code())?;
51        writer.field_bytes(4, &self.report_hash)?;
52        writer.field_u32(5, self.finding_count)?;
53        Ok(())
54    }
55}
56
57/// Attestation payload. FDD-03 §9.9.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct AttestationPayload {
60    /// Target block ID.
61    pub target_block_id: ObjectId,
62    /// Policy version string.
63    pub policy_version: String,
64    /// Plugin-set hash.
65    pub plugin_set_hash: Vec<u8>,
66    /// Results sorted by plugin ID.
67    pub results: Vec<PluginResultEntry>,
68    /// Overall status.
69    pub status: AttestationStatus,
70    /// Authoritative attestation creation timestamp.
71    pub created_at: u64,
72    /// True if this result can be reproduced offline from stored inputs.
73    pub is_reproducible_offline: bool,
74}
75
76/// FDD-03 §9.9 sort key for `results`: strictly ascending by `plugin_id` UTF-8
77/// bytes. Strictness also forbids duplicate `plugin_id` values. No secondary key is
78/// used in v1, so later fields never participate in canonical ordering.
79fn results_sorted_by_plugin_id(results: &[PluginResultEntry]) -> bool {
80    results.windows(2).all(|pair| match pair {
81        [a, b] => a.plugin_id.as_bytes() < b.plugin_id.as_bytes(),
82        _ => true,
83    })
84}
85
86impl CanonicalEncode for AttestationPayload {
87    fn encode_canonical(&self, writer: &mut CanonicalWriter) -> Result<()> {
88        if !results_sorted_by_plugin_id(&self.results) {
89            return Err(PrikkError::CanonicalEncoding(
90                "plugin results must be strictly ordered and unique by plugin_id".to_string(),
91            ));
92        }
93        writer.field_object_id(1, &self.target_block_id)?;
94        writer.field_string(2, &self.policy_version)?;
95        writer.field_bytes(3, &self.plugin_set_hash)?;
96        writer.repeated_record_list(4, &self.results)?;
97        writer.field_enum_u16(5, self.status.code())?;
98        writer.field_u64(6, self.created_at)?;
99        writer.field_bool(7, self.is_reproducible_offline)?;
100        Ok(())
101    }
102}