Skip to main content

tf_types/
envelope.rs

1//! Signature envelope shape validator — mirrors
2//! `tools/tf-types-ts/src/core/envelope.ts`. No crypto is performed here;
3//! real signing/verification lives in `crypto.rs`.
4
5use crate::generated::common::SignatureEnvelope;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum EnvelopeIssue {
9    MissingAlgorithm,
10    MissingSigner,
11    MissingSignature,
12    InvalidBase64 { field: &'static str },
13    AltWithoutAlgorithm,
14    UnknownAlgorithm { algorithm: String },
15    UnknownAltAlgorithm { algorithm: String },
16}
17
18impl EnvelopeIssue {
19    pub fn is_warning(&self) -> bool {
20        matches!(
21            self,
22            EnvelopeIssue::UnknownAlgorithm { .. } | EnvelopeIssue::UnknownAltAlgorithm { .. }
23        )
24    }
25}
26
27pub struct EnvelopeValidation {
28    pub ok: bool,
29    pub issues: Vec<EnvelopeIssue>,
30}
31
32const KNOWN_ALGORITHMS: &[&str] = &[
33    "ed25519",
34    "ed448",
35    "p256",
36    "p384",
37    "p521",
38    "rsa-pss-sha256",
39    "ml-dsa-44",
40    "ml-dsa-65",
41    "ml-dsa-87",
42    "slh-dsa-sha2-128s",
43    "slh-dsa-sha2-192s",
44];
45
46pub fn validate_envelope_shape(e: &SignatureEnvelope) -> EnvelopeValidation {
47    let mut issues = Vec::new();
48
49    if e.algorithm.is_empty() {
50        issues.push(EnvelopeIssue::MissingAlgorithm);
51    }
52    if e.signer.is_empty() {
53        issues.push(EnvelopeIssue::MissingSigner);
54    }
55    if e.signature.is_empty() {
56        issues.push(EnvelopeIssue::MissingSignature);
57    }
58    if !e.signature.is_empty() && !is_base64(&e.signature) {
59        issues.push(EnvelopeIssue::InvalidBase64 { field: "signature" });
60    }
61    if let Some(alt) = &e.alt_signature {
62        if !is_base64(alt) {
63            issues.push(EnvelopeIssue::InvalidBase64 {
64                field: "alt_signature",
65            });
66        }
67        if e.alt_algorithm.is_none() {
68            issues.push(EnvelopeIssue::AltWithoutAlgorithm);
69        }
70    }
71
72    let fatal_count = issues.iter().filter(|i| !i.is_warning()).count();
73
74    if !e.algorithm.is_empty() && !KNOWN_ALGORITHMS.contains(&e.algorithm.as_str()) {
75        issues.push(EnvelopeIssue::UnknownAlgorithm {
76            algorithm: e.algorithm.clone(),
77        });
78    }
79    if let Some(alt) = &e.alt_algorithm {
80        if !KNOWN_ALGORITHMS.contains(&alt.as_str()) {
81            issues.push(EnvelopeIssue::UnknownAltAlgorithm {
82                algorithm: alt.clone(),
83            });
84        }
85    }
86
87    EnvelopeValidation {
88        ok: fatal_count == 0,
89        issues,
90    }
91}
92
93fn is_base64(s: &str) -> bool {
94    if s.is_empty() {
95        return false;
96    }
97    let mut eq_seen = 0;
98    for c in s.chars() {
99        match c {
100            'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' => {
101                if eq_seen > 0 {
102                    return false;
103                }
104            }
105            '=' => {
106                eq_seen += 1;
107                if eq_seen > 2 {
108                    return false;
109                }
110            }
111            _ => return false,
112        }
113    }
114    true
115}