koprogo_api/domain/plateforme/
qualified_signature.rs1use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18use uuid::Uuid;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum SignatureProviderKind {
24 Eid,
27 Itsme,
29 Universign,
31}
32
33impl SignatureProviderKind {
34 pub fn as_db_str(&self) -> &'static str {
36 match self {
37 Self::Eid => "eid",
38 Self::Itsme => "itsme",
39 Self::Universign => "universign",
40 }
41 }
42
43 pub fn from_db_str(s: &str) -> Option<Self> {
44 match s {
45 "eid" => Some(Self::Eid),
46 "itsme" => Some(Self::Itsme),
47 "universign" => Some(Self::Universign),
48 _ => None,
49 }
50 }
51}
52
53impl std::fmt::Display for SignatureProviderKind {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{}", self.as_db_str())
56 }
57}
58
59pub fn select_signature_provider(
67 cabinet_preference: SignatureProviderKind,
68 subject_is_belgian: bool,
69) -> SignatureProviderKind {
70 if subject_is_belgian {
71 cabinet_preference
72 } else {
73 SignatureProviderKind::Universign
74 }
75}
76
77#[derive(Debug, Error, Clone, PartialEq, Eq)]
79pub enum QualifiedSignatureError {
80 #[error(
81 "Le condensat du document doit être un HMAC-SHA256 hexadécimal (64 caractères), \
82 reçu {0} caractère(s)"
83 )]
84 DocumentHashInvalidLength(usize),
85
86 #[error("Le condensat du document contient des caractères non hexadécimaux")]
87 DocumentHashNotHex,
88
89 #[error("La référence prestataire ne peut pas être vide")]
90 ProviderReferenceEmpty,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct QualifiedSignature {
97 pub id: Uuid,
98 pub provider: SignatureProviderKind,
99 pub subject_user_id: Uuid,
100 pub document_id: Uuid,
101 pub document_hash: String,
104 pub provider_reference: String,
106 pub signed_at: DateTime<Utc>,
107 pub created_at: DateTime<Utc>,
108}
109
110impl QualifiedSignature {
111 pub fn new(
112 provider: SignatureProviderKind,
113 subject_user_id: Uuid,
114 document_id: Uuid,
115 document_hash: String,
116 provider_reference: String,
117 signed_at: DateTime<Utc>,
118 ) -> Result<Self, QualifiedSignatureError> {
119 if document_hash.len() != 64 {
120 return Err(QualifiedSignatureError::DocumentHashInvalidLength(
121 document_hash.len(),
122 ));
123 }
124 if !document_hash.chars().all(|c| c.is_ascii_hexdigit()) {
125 return Err(QualifiedSignatureError::DocumentHashNotHex);
126 }
127 let provider_reference = provider_reference.trim().to_string();
128 if provider_reference.is_empty() {
129 return Err(QualifiedSignatureError::ProviderReferenceEmpty);
130 }
131
132 Ok(Self {
133 id: Uuid::new_v4(),
134 provider,
135 subject_user_id,
136 document_id,
137 document_hash,
138 provider_reference,
139 signed_at,
140 created_at: Utc::now(),
141 })
142 }
143}
144
145#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn valid_hash() -> String {
154 "a".repeat(64)
155 }
156
157 #[test]
162 fn happy_new_builds_a_valid_qualified_signature() {
163 let sig = QualifiedSignature::new(
164 SignatureProviderKind::Eid,
165 Uuid::new_v4(),
166 Uuid::new_v4(),
167 valid_hash(),
168 "ref-123".to_string(),
169 Utc::now(),
170 )
171 .unwrap();
172 assert_eq!(sig.provider, SignatureProviderKind::Eid);
173 assert_eq!(sig.provider_reference, "ref-123");
174 }
175
176 #[test]
177 fn happy_belgian_subject_gets_cabinet_preference() {
178 let selected = select_signature_provider(SignatureProviderKind::Itsme, true);
179 assert_eq!(selected, SignatureProviderKind::Itsme);
180 }
181
182 #[test]
183 fn happy_universign_preference_is_never_overridden() {
184 let selected = select_signature_provider(SignatureProviderKind::Universign, true);
185 assert_eq!(selected, SignatureProviderKind::Universign);
186 }
187
188 #[test]
193 fn edge_non_belgian_subject_falls_back_to_universign_even_if_itsme_preferred() {
194 let selected = select_signature_provider(SignatureProviderKind::Itsme, false);
195 assert_eq!(selected, SignatureProviderKind::Universign);
196 }
197
198 #[test]
199 fn edge_non_belgian_subject_falls_back_to_universign_even_if_eid_preferred() {
200 let selected = select_signature_provider(SignatureProviderKind::Eid, false);
203 assert_eq!(selected, SignatureProviderKind::Universign);
204 }
205
206 #[test]
207 fn edge_document_hash_must_be_exactly_64_chars() {
208 let err = QualifiedSignature::new(
209 SignatureProviderKind::Eid,
210 Uuid::new_v4(),
211 Uuid::new_v4(),
212 "a".repeat(63),
213 "ref".to_string(),
214 Utc::now(),
215 )
216 .unwrap_err();
217 assert_eq!(err, QualifiedSignatureError::DocumentHashInvalidLength(63));
218 }
219
220 #[test]
225 fn security_document_hash_must_be_hex_only() {
226 let err = QualifiedSignature::new(
230 SignatureProviderKind::Itsme,
231 Uuid::new_v4(),
232 Uuid::new_v4(),
233 "z".repeat(64),
234 "ref".to_string(),
235 Utc::now(),
236 )
237 .unwrap_err();
238 assert_eq!(err, QualifiedSignatureError::DocumentHashNotHex);
239 }
240
241 #[test]
246 fn negative_empty_provider_reference_is_rejected() {
247 let err = QualifiedSignature::new(
248 SignatureProviderKind::Universign,
249 Uuid::new_v4(),
250 Uuid::new_v4(),
251 valid_hash(),
252 " ".to_string(),
253 Utc::now(),
254 )
255 .unwrap_err();
256 assert_eq!(err, QualifiedSignatureError::ProviderReferenceEmpty);
257 }
258
259 #[test]
260 fn negative_from_db_str_unknown_value_returns_none_not_panic() {
261 assert_eq!(SignatureProviderKind::from_db_str("docusign"), None);
262 }
263}