prikk_store/signature_diagnostics.rs
1//! Signature-envelope shape/order diagnostics: malformed algorithm shape, duplicate signature
2//! tuples, and non-canonical ordering, reported as separate `SignatureEnvelopeIssue`s alongside
3//! (not instead of) the hard rejection `ObjectEnvelope::validate_strict` already performs for the
4//! same three conditions. See `classify_signature_envelope`'s own doc for why, post-RFC-103, that
5//! makes this layer's non-empty-result path provably unreachable through `verify_repository`'s
6//! pipeline, and why the code stays regardless (DC-95 Stage 1 round 6's ruling on unreachable
7//! checks).
8
9use std::fmt;
10
11use prikk_error::Result;
12use prikk_object::{ObjectEnvelope, ObjectId, ObjectType};
13
14/// Persisted source of a signature-envelope diagnostic.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SignatureEnvelopeSource {
17 /// Content-addressed object file.
18 Object {
19 /// Object type, ordered by its numeric registry code.
20 object_type: ObjectType,
21 /// Object identifier, ordered by its raw bytes.
22 object_id: ObjectId,
23 },
24 /// Active-session WAL record.
25 ActiveWal {
26 /// WAL sequence number.
27 sequence: u64,
28 /// Envelope object identifier.
29 object_id: ObjectId,
30 },
31 /// Inline ref-log record.
32 RefLog {
33 /// Canonical ref name.
34 ref_name: String,
35 /// One-based record sequence within the ref log.
36 sequence: u64,
37 /// RefUpdate object identifier.
38 object_id: ObjectId,
39 },
40}
41
42impl fmt::Display for SignatureEnvelopeSource {
43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Object {
46 object_type,
47 object_id,
48 } => write!(formatter, "object {object_type} {object_id}"),
49 Self::ActiveWal {
50 sequence,
51 object_id,
52 } => write!(
53 formatter,
54 "active WAL sequence {sequence} object {object_id}"
55 ),
56 Self::RefLog {
57 ref_name,
58 sequence,
59 object_id,
60 } => write!(
61 formatter,
62 "ref log {ref_name} sequence {sequence} object {object_id}"
63 ),
64 }
65 }
66}
67
68/// One warning-level non-canonical signature-envelope condition.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct SignatureEnvelopeIssue {
71 /// Stable diagnostic code.
72 pub code: &'static str,
73 /// Persisted envelope source.
74 pub source: SignatureEnvelopeSource,
75 /// Human-readable diagnosis without host paths.
76 pub message: String,
77}
78
79/// RFC 103: every one of this function's three call sites (`verify.rs`, `verify/objects.rs`,
80/// `refs/verify.rs`) runs it immediately after a `crate::format::validate_read_schema(layout.format(),
81/// &envelope)?` on the same envelope, propagating on `Err` before this function is ever reached.
82/// `validate_read_schema` under `RepositoryFormat::CurrentV6` calls `envelope.validate_strict()`,
83/// which independently checks the exact same three conditions this function classifies
84/// (`signature_issues()`'s `malformed_shape`/`duplicate`/`noncanonical_order`) and hard-errors on any
85/// of them. With format-1 and format-2 both retired -- format-1's `validate_read_schema` branch
86/// checked only `schema_version`, never calling `validate_strict()` -- `CurrentV6` is the only format
87/// left, so any envelope this function would flag was already rejected one call earlier. **Provably
88/// unreachable through `verify_repository`'s pipeline**, the same shape as the rollback
89/// wrong-signature-length
90/// check (DC-95 Stage 1 round 11), but not a downgrade of a blocking check: Stage 1 already classified
91/// `signature_envelope_issues` from every source as "Excluded" -- it never backed a blocking
92/// predicate, for any source, even before this. Kept, untested, with the argument recorded (round 6's
93/// ruling on unreachable checks), since a caller could still construct an envelope directly and read
94/// its issues without going through `verify_repository` at all.
95pub(crate) fn classify_signature_envelope(
96 envelope: &ObjectEnvelope,
97 source: SignatureEnvelopeSource,
98) -> Result<Vec<SignatureEnvelopeIssue>> {
99 let conditions = envelope.signature_issues()?;
100 let mut issues = Vec::with_capacity(3);
101 if conditions.malformed_shape {
102 issues.push(issue(
103 "PRIKK-VERIFY-SIGNATURE-MALFORMED",
104 &source,
105 "envelope contains a signature with malformed algorithm shape",
106 ));
107 }
108 if conditions.duplicate {
109 issues.push(issue(
110 "PRIKK-VERIFY-SIGNATURE-DUPLICATE",
111 &source,
112 "envelope contains a duplicate signature tuple",
113 ));
114 }
115 if conditions.noncanonical_order {
116 issues.push(issue(
117 "PRIKK-VERIFY-SIGNATURE-NONCANONICAL-ORDER",
118 &source,
119 "envelope signatures are not in canonical order",
120 ));
121 }
122 Ok(issues)
123}
124
125fn issue(
126 code: &'static str,
127 source: &SignatureEnvelopeSource,
128 message: &str,
129) -> SignatureEnvelopeIssue {
130 SignatureEnvelopeIssue {
131 code,
132 source: source.clone(),
133 message: message.to_string(),
134 }
135}