Skip to main content

xml_sec/xmldsig/verify/session/
hash_uses.rs

1//! Paid projection of original selected signature and digest decisions.
2
3use super::*;
4
5/// The instruction's exact hash role, independent of payload acceptance.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum HashUseRole {
8    /// External digest of canonical SignedInfo, or an intrinsic signature suite.
9    SignatureMessage,
10    /// RSA-PSS's separately selected MGF1 hash.
11    MaskGeneration,
12    /// Digest of an authenticated Reference's transformed object.
13    Reference(ReferenceParent),
14}
15
16#[cfg(test)]
17mod tests {
18    use super::*;
19
20    #[test]
21    fn every_advertised_method_has_explicit_message_hash_semantics() {
22        // A newly advertised signature method must not silently disappear from
23        // algorithm projection, including pure experimental suites.
24        for algorithm in SignatureAlgorithm::ALL {
25            assert!(
26                !matches!(message_hash(algorithm), HashUseAlgorithm::Unsupported(_)),
27                "{algorithm:?}"
28            );
29        }
30    }
31}
32
33/// Declared hash semantics; output length never determines the algorithm.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum HashUseAlgorithm<'a> {
36    /// A recognized fixed-output digest.
37    Digest(DigestAlgorithm),
38    /// RFC 8032 Ed448ph's SHAKE256 prehash with a 64-byte output.
39    Shake256_512,
40    /// No independently selected external digest (e.g. PureEdDSA or pure ML-DSA).
41    Intrinsic(SignatureAlgorithm),
42    /// Original unimplemented/malformed instruction, including an absent URI.
43    Unsupported(Option<&'a str>),
44}
45
46/// Whether this original role actually completed its hash operation.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum HashUseState {
49    /// The original operation successfully computed the digest or signature suite.
50    Complete,
51    /// Original algorithm semantics could not be projected.
52    Unsupported,
53    /// Declared instructions were not completed, regardless of the reason.
54    Unprocessed,
55}
56
57/// One original role, never reconstructed from result lengths or key metadata.
58#[derive(Debug)]
59pub struct SignatureHashUse<'a> {
60    /// Original selected Signature, including document and generation.
61    pub signature: NodeIdentity,
62    /// Original SignatureMethod or Reference instruction.
63    pub instruction: NodeIdentity,
64    /// Original method element, absent only for a malformed Reference layout.
65    pub method: Option<NodeIdentity>,
66    /// Semantic role and, for Reference, exact authenticated parent.
67    pub role: HashUseRole,
68    /// Declared typed algorithm or original unsupported spelling.
69    pub algorithm: HashUseAlgorithm<'a>,
70    /// Actual original execution disposition, not a payload/trust verdict.
71    pub state: HashUseState,
72}
73
74/// Overall completeness; every individual role retains its own disposition.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum HashUseCompleteness {
77    /// Every original authenticated role was projected and completed.
78    Complete,
79    /// An original role cannot be interpreted by this release.
80    Unsupported(NodeIdentity),
81    /// At least one original authenticated instruction was not completed.
82    Unprocessed(NodeIdentity),
83}
84
85fn message_hash(algorithm: SignatureAlgorithm) -> HashUseAlgorithm<'static> {
86    if let Some(parameters) = algorithm.rsa_pss_parameters() {
87        return HashUseAlgorithm::Digest(parameters.digest);
88    }
89    if let Some(digest) = algorithm.ecdsa_digest() {
90        return HashUseAlgorithm::Digest(digest);
91    }
92    use DigestAlgorithm as D;
93    use SignatureAlgorithm as S;
94    let digest = match algorithm {
95        S::RsaSha1 | S::DsaSha1 | S::HmacSha1 => D::Sha1,
96        S::RsaSha224 | S::HmacSha224 => D::Sha224,
97        S::RsaSha256 | S::DsaSha256 | S::HmacSha256 => D::Sha256,
98        S::RsaSha384 | S::HmacSha384 => D::Sha384,
99        S::RsaSha512 | S::HmacSha512 | S::Ed25519Ph => D::Sha512,
100        #[cfg(feature = "legacy-algorithms")]
101        S::RsaMd5 | S::HmacMd5 => D::Md5,
102        #[cfg(feature = "legacy-algorithms")]
103        S::RsaRipemd160 | S::HmacRipemd160 => D::Ripemd160,
104        // RFC 8032 §§3,5.1,5.2: PureEdDSA has PH(M)=M; Ed448ph uses
105        // SHAKE256(M,64), not SHA3-512 or a digest inferred from 64 bytes.
106        // https://www.rfc-editor.org/rfc/rfc8032#section-5.2
107        S::Ed448Ph => return HashUseAlgorithm::Shake256_512,
108        S::Ed25519 | S::Ed25519Ctx | S::Ed448 | S::PostQuantum(_) => {
109            return HashUseAlgorithm::Intrinsic(algorithm);
110        }
111        _ => return HashUseAlgorithm::Unsupported(Some(algorithm.uri())),
112    };
113    HashUseAlgorithm::Digest(digest)
114}
115
116// A method (rather than a captured closure) accepts each attribute's fresh
117// scoped lifetime without retaining its spelling or allocating an owned copy.
118struct ReferenceHashVisitor<'a, W, F> {
119    signature: NodeIdentity,
120    instruction: NodeIdentity,
121    parent: ReferenceParent,
122    completed: bool,
123    work: &'a W,
124    completeness: &'a mut HashUseCompleteness,
125    visitor: &'a mut F,
126}
127
128impl<W: Work, F> ReferenceHashVisitor<'_, W, F> {
129    fn emit<E>(
130        &mut self,
131        algorithm: HashUseAlgorithm<'_>,
132        method: Option<NodeIdentity>,
133    ) -> Result<(), CatalogueError<E>>
134    where
135        F: for<'a> FnMut(SignatureHashUse<'a>) -> Result<(), E>,
136    {
137        let state = if matches!(algorithm, HashUseAlgorithm::Unsupported(_)) {
138            *self.completeness = HashUseCompleteness::Unsupported(self.instruction);
139            HashUseState::Unsupported
140        } else if self.completed {
141            HashUseState::Complete
142        } else {
143            if matches!(self.completeness, HashUseCompleteness::Complete) {
144                *self.completeness = HashUseCompleteness::Unprocessed(self.instruction);
145            }
146            HashUseState::Unprocessed
147        };
148        self.work
149            .charge_work(1)
150            .map_err(|error| CatalogueError::Processing(error.into()))?;
151        (self.visitor)(SignatureHashUse {
152            signature: self.signature,
153            instruction: self.instruction,
154            method,
155            role: HashUseRole::Reference(self.parent),
156            algorithm,
157            state,
158        })
159        .map_err(CatalogueError::Visitor)
160    }
161}
162
163impl<W: Work, Q: Work> SelectedSignature<'_, '_, '_, '_, W, Q> {
164    /// Original typed method captured during the first successful verification.
165    pub fn signature_method(&self) -> Result<SelectedSignatureMethod, DsigError> {
166        self.session.signature_method_in(self.index, self.work)
167    }
168
169    /// Visit declared original roles and their actual original completion state.
170    /// This neither verifies again nor resolves keys or resources again.
171    pub fn visit_hash_uses<E>(
172        &self,
173        mut visitor: impl for<'a> FnMut(SignatureHashUse<'a>) -> Result<(), E>,
174    ) -> Result<HashUseCompleteness, CatalogueError<E>> {
175        let method = self
176            .signature_method()
177            .map_err(CatalogueError::Processing)?;
178        let signature = self.session.evidence.signatures()[self.index].identity;
179        let mut completeness = HashUseCompleteness::Complete;
180        let algorithm = message_hash(method.algorithm);
181        let state = if matches!(algorithm, HashUseAlgorithm::Unsupported(_)) {
182            completeness = HashUseCompleteness::Unsupported(method.identity);
183            HashUseState::Unsupported
184        } else {
185            HashUseState::Complete
186        };
187        self.work
188            .charge_work(1)
189            .map_err(|error| CatalogueError::Processing(error.into()))?;
190        visitor(SignatureHashUse {
191            signature,
192            instruction: method.identity,
193            method: Some(method.identity),
194            role: HashUseRole::SignatureMessage,
195            algorithm,
196            state,
197        })
198        .map_err(CatalogueError::Visitor)?;
199        // RFC 9231 §2.3.9 and RFC 8017 §9.1: MGF1's digest is independent
200        // of the message digest. Use the originally parsed effective parameters.
201        // https://www.rfc-editor.org/rfc/rfc9231#section-2.3.9
202        if let Some(parameters) = method.algorithm.rsa_pss_parameters() {
203            self.work
204                .charge_work(1)
205                .map_err(|error| CatalogueError::Processing(error.into()))?;
206            visitor(SignatureHashUse {
207                signature,
208                instruction: method.identity,
209                method: Some(method.identity),
210                role: HashUseRole::MaskGeneration,
211                algorithm: HashUseAlgorithm::Digest(parameters.mgf_digest),
212                state: HashUseState::Complete,
213            })
214            .map_err(CatalogueError::Visitor)?;
215        }
216        let catalogue = self.visit_reference_instructions(|node, parent, method| {
217            let reference = self.session.view.node_identity(node);
218            let digest_receipt = self.session.operations[self.index]
219                .capture
220                .digests
221                .borrow()
222                .get(&node.id().get_usize(), self.work)
223                .map_err(|error| CatalogueError::Processing(error.into()))?
224                .copied();
225            let mut emitter = ReferenceHashVisitor {
226                signature,
227                instruction: reference,
228                parent,
229                completed: digest_receipt.is_some_and(|receipt| receipt.computed),
230                work: self.work,
231                completeness: &mut completeness,
232                visitor: &mut visitor,
233            };
234            let Some(method) = method else {
235                return emitter.emit(HashUseAlgorithm::Unsupported(None), None);
236            };
237            let method_identity = self.session.view.node_identity(method);
238            if let Some(receipt) = digest_receipt {
239                return emitter.emit(
240                    HashUseAlgorithm::Digest(receipt.algorithm),
241                    Some(method_identity),
242                );
243            }
244            let mut found = false;
245            let inspected = super::super::super::extension::visit_direct_attributes(
246                self.session.view,
247                method_identity,
248                self.work,
249                |attribute| {
250                    if attribute.namespace.is_none() && attribute.local_name == "Algorithm" {
251                        found = true;
252                        // Bound every possible registry comparison before interpreting
253                        // an instruction the original execution never parsed.
254                        let comparisons = DigestAlgorithm::ALL
255                            .iter()
256                            .map(|algorithm| algorithm.uri().len() as u64 + 1)
257                            .sum();
258                        self.work
259                            .charge_work(comparisons)
260                            .map_err(|error| CatalogueError::Processing(error.into()))?;
261                        let algorithm = DigestAlgorithm::from_uri(attribute.value).map_or(
262                            HashUseAlgorithm::Unsupported(Some(attribute.value)),
263                            HashUseAlgorithm::Digest,
264                        );
265                        emitter.emit(algorithm, Some(method_identity))?;
266                    }
267                    Ok(())
268                },
269            );
270            match inspected {
271                Ok(()) => {}
272                Err(super::super::super::extension::InspectionError::Visitor(error)) => {
273                    return Err(error);
274                }
275                Err(super::super::super::extension::InspectionError::Processing(error)) => {
276                    return Err(CatalogueError::Processing(match error {
277                        super::super::super::extension::ScalarError::Resource(error) => {
278                            error.into()
279                        }
280                        super::super::super::extension::ScalarError::Document(error) => {
281                            error.into()
282                        }
283                        super::super::super::extension::ScalarError::NotElement => {
284                            DsigError::InvalidRequest {
285                                reason: "DigestMethod is not an element",
286                            }
287                        }
288                    }));
289                }
290            }
291            if !found {
292                emitter.emit(HashUseAlgorithm::Unsupported(None), Some(method_identity))?;
293            }
294            Ok(())
295        });
296        match catalogue {
297            Err(CatalogueError::Visitor(error)) => Err(error),
298            Err(CatalogueError::Processing(error)) => Err(CatalogueError::Processing(error)),
299            Ok(status) => {
300                if matches!(completeness, HashUseCompleteness::Complete) {
301                    match status {
302                        CatalogueCompleteness::Complete => {}
303                        CatalogueCompleteness::Incomplete(node)
304                        | CatalogueCompleteness::Unchecked(node) => {
305                            completeness = HashUseCompleteness::Unprocessed(node)
306                        }
307                    }
308                }
309                Ok(completeness)
310            }
311        }
312    }
313}