1use 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}