1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Wilder Management Inc. (d/b/a Wilder Robotics) <rob@wilder-robotics.com>
// pask-wire is licensed Apache-2.0. No commercial agreement is required to use,
// modify or redistribute it; see LICENSING.md in the workspace root.
//! Chain-Verifier: the chain-level checks that apply when two or more receipts
//! are presented as one contiguous chain.
//!
//! `-01` ยง4.1 adds two normative requirements on a Chain-Verifier presented
//! with such a presentation:
//!
//! 1. MUST check `chain.seq` contiguity across the presentation.
//! 2. MUST check each receipt's `chain.prevHash` equals the preceding
//! receipt's `chain.hash`.
//!
//! [`Payload::from_json`](crate::Payload::from_json) already validates every
//! individual receipt on its own, including the seq0/prevHash-null pairing
//! and that a receipt's own `chain.hash` matches its content. This module
//! does not repeat that work: [`verify_chain`] does NOT re-verify each
//! receipt's own `chain.hash`, because parsing already did. It only checks
//! the relationships *between* adjacent receipts in a presentation.
//!
//! `-02` adds a third chain-level rule, over `issuerAffiliation`, and it is
//! deliberately not a rejection.
//!
//! The two `-01` checks are structural. Sequence numbering and the hash link
//! are entirely under the Issuer's control, so a violation of either is always
//! an error or tampering, with no honest explanation available. Affiliation is
//! not structural. It is a claim about the world outside the receipt, and the
//! world changes: an Issuer independent of the Site Owner in March can be
//! acquired by that Site Owner in September. Treating that as a malformed chain
//! would put an ordinary corporate event into the same bucket as tampering, and
//! would report it to a relying party in the same words.
//!
//! It would also be destructive. The only way to comply with a rejection rule
//! is to start a new chain, which resets `seq` to zero and `prevHash` to null,
//! and so deletes the link between the receipts from before the change and the
//! ones after. That is exactly the continuity a chain exists to carry.
//!
//! So [`verify_chain`] returns a [`ChainReport`] rather than a bare unit. A
//! chain whose `issuerAffiliation` changes is still a valid chain, and the
//! report names every point at which the value changed, identified by the
//! `chain.seq` of the receipt that changed it.
//!
//! What is prohibited is collapsing the presentation to one affiliation value.
//! That prohibition, not rejection, is what closes the relabelling attack: the
//! resolution a reader reaches for is to take the value from the newest
//! receipt, and doing that lets a whole chain be relabelled after the fact by
//! appending a single receipt, with nothing in the record showing the label
//! ever said anything else. Every reported value stays attached to the receipts
//! that actually carry it.
use crate::;
use Vec;
/// One point inside a presentation at which `issuerAffiliation` changed.
///
/// `at_seq` is the `chain.seq` of the receipt carrying the new value, so a
/// report can be quoted against the presentation without recounting positions.
/// What a Chain-Verifier observed about a presentation it accepted.
///
/// An empty report is the ordinary case and means the presentation was
/// structurally sound and uniform in its `issuerAffiliation`.
///
/// There is deliberately no accessor returning a single affiliation value for
/// the chain. A caller wanting to know what a given receipt claims reads that
/// receipt. Supplying one value for the whole presentation is the collapse this
/// type exists to prevent.
/// Verifies a slice of receipts as one contiguous chain.
///
/// This checks only the chain-level relationships between adjacent receipts.
/// It does NOT re-verify any receipt's own `chain.hash` against its content.
/// `Payload::from_json` already did that at parse time, for every receipt in
/// the slice.
///
/// Rules, applied in this order:
///
/// - An empty slice is rejected.
/// - The head of the presentation (`receipts[0]`) MUST carry `seq == 0` and
/// `prevHash == None`. Per-receipt validation already enforces the
/// seq0/prevHash-null pairing in general; this is the additional
/// chain-level rule that the *head of a presentation* specifically must be
/// sequence zero.
/// - For each adjacent pair, `seq` MUST be contiguous: `seq[i] == seq[i-1] +
/// 1`.
/// - For each adjacent pair, `prevHash[i]` MUST equal `Some(hash[i-1])`.
/// - A change in `issuerAffiliation` between adjacent receipts is recorded in
/// the returned [`ChainReport`] and does NOT invalidate the presentation.
/// See the module documentation for why this one is not a rejection.
///
/// # Errors
///
/// Returns [`Error::Validation`] on the first structural rule violated, in the
/// order listed above.