1use crate::config::EnvironmentType;
3use base64ct::{Base64, Encoding};
4use ecdsa;
5use fatoora_derive::Validate;
6use java_properties::read;
7use k256::{Secp256k1, ecdsa::SigningKey as K256SigningKey};
8use k256::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding as KeyLineEnding};
9use std::{
10 io::Cursor,
11 path::{self, PathBuf},
12 str::FromStr,
13 vec,
14};
15use thiserror::Error;
16use x509_cert::{
17 builder::{Builder, RequestBuilder},
18 der::{
19 Decode, Encode, EncodePem, Error as DerError, Length, Result as DerResult, Writer, asn1,
20 pem::LineEnding as CsrLineEnding,
21 },
22 ext::{
23 AsExtension, Extension,
24 pkix::{SubjectAltName, name::GeneralName},
25 },
26 name,
27 request::CertReq,
28};
29
30#[derive(Debug, Error)]
32pub enum CsrError {
33 #[error("failed to open CSR config file '{path}': {source}")]
34 Io {
35 path: PathBuf,
36 #[source]
37 source: std::io::Error,
38 },
39
40 #[error("failed to parse CSR properties from '{path}': {source}")]
41 PropertiesRead {
42 path: PathBuf,
43 #[source]
44 source: java_properties::PropertiesError,
45 },
46
47 #[error("missing required CSR property '{key}' in file '{path}'")]
48 MissingProperty { path: PathBuf, key: String },
49
50 #[error("invalid subject distinguished name constructed from provided fields: {message}")]
51 InvalidSubject { message: String },
52
53 #[error("invalid Subject Alternative Name (SAN) from fields: {message}")]
54 InvalidSan { message: String },
55
56 #[error("failed to construct CSR request: {message}")]
57 RequestBuild { message: String },
58
59 #[error("failed adding CSR extension '{which}': {message}")]
60 AddExtension {
61 which: &'static str,
62 message: String,
63 },
64
65 #[error("failed to build CSR: {message}")]
66 CsrBuild { message: String },
67
68 #[error("failed to decode signing key: {message}")]
69 KeyDecode { message: String },
70
71 #[error("failed to encode signing key: {message}")]
72 KeyEncode { message: String },
73
74 #[error("failed DER encoding for {context}: {source}")]
75 DerEncode {
76 context: &'static str,
77 #[source]
78 source: DerError,
79 },
80
81 #[error("validation error: {message}")]
82 Validation { message: String },
83}
84
85struct TemplateNameExtension(pub asn1::OctetString);
86
87impl const_oid::AssociatedOid for TemplateNameExtension {
88 const OID: const_oid::ObjectIdentifier =
89 const_oid::ObjectIdentifier::new_unwrap("1.3.6.1.4.1.311.20.2");
90}
91
92impl Encode for TemplateNameExtension {
93 fn encoded_len(&self) -> DerResult<Length> {
94 self.0.encoded_len()
95 }
96 fn encode(&self, encoder: &mut impl Writer) -> DerResult<()> {
97 self.0.encode(encoder)
98 }
99}
100
101impl AsExtension for TemplateNameExtension {
102 fn critical(&self, _name: &name::Name, _exts: &[Extension]) -> bool {
103 false
104 }
105}
106
107impl EnvironmentType {
108 const fn as_template_bytes(&self) -> &'static [u8] {
109 match self {
110 EnvironmentType::NonProduction => b"TSTZATCA-Code-Signing",
111 EnvironmentType::Simulation => b"PREZATCA-Code-Signing",
112 EnvironmentType::Production => b"ZATCA-Code-Signing",
113 }
114 }
115
116 fn to_extension(self) -> Result<TemplateNameExtension, CsrError> {
117 let bytes = self.as_template_bytes();
118 let os = asn1::OctetString::new(bytes).map_err(|e| CsrError::RequestBuild {
119 message: format!("invalid template name bytes for extension: {e}"),
120 })?;
121 Ok(TemplateNameExtension(os))
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct SigningKey {
128 inner: K256SigningKey,
129}
130
131impl SigningKey {
132 pub fn generate() -> Self {
133 Self {
134 inner: ecdsa::SigningKey::<Secp256k1>::generate(),
135 }
136 }
137
138 pub fn from_der(der: &[u8]) -> Result<Self, CsrError> {
139 let inner = K256SigningKey::from_pkcs8_der(der).map_err(|e| CsrError::KeyDecode {
140 message: e.to_string(),
141 })?;
142 Ok(Self { inner })
143 }
144
145 pub fn from_pem(pem: &str) -> Result<Self, CsrError> {
146 let inner = K256SigningKey::from_pkcs8_pem(pem).map_err(|e| CsrError::KeyDecode {
147 message: e.to_string(),
148 })?;
149 Ok(Self { inner })
150 }
151
152 pub fn to_der(&self) -> Result<Vec<u8>, CsrError> {
153 let doc = self
154 .inner
155 .to_pkcs8_der()
156 .map_err(|e: k256::pkcs8::Error| CsrError::KeyEncode {
157 message: e.to_string(),
158 })?;
159 Ok(doc.as_bytes().to_vec())
160 }
161
162 pub fn to_pem(&self) -> Result<String, CsrError> {
163 let pem = self
164 .inner
165 .to_pkcs8_pem(KeyLineEnding::LF)
166 .map_err(|e: k256::pkcs8::Error| CsrError::KeyEncode {
167 message: e.to_string(),
168 })?;
169 Ok(pem.to_string())
170 }
171
172 pub(crate) fn inner(&self) -> &K256SigningKey {
173 &self.inner
174 }
175}
176
177#[derive(Debug, Clone)]
179pub struct Csr {
180 inner: CertReq,
181}
182
183impl Csr {
184 pub fn from_der(der: &[u8]) -> Result<Self, CsrError> {
185 let inner = CertReq::from_der(der).map_err(|e| CsrError::DerEncode {
186 context: "certificate request (DER)",
187 source: e,
188 })?;
189 Ok(Self { inner })
190 }
191
192 pub fn to_der(&self) -> Result<Vec<u8>, CsrError> {
193 let der_bytes = self.inner.to_der().map_err(|e| CsrError::DerEncode {
194 context: "certificate request",
195 source: e,
196 })?;
197 Ok(der_bytes)
198 }
199
200 pub fn to_pem(&self) -> Result<String, CsrError> {
201 let pem = self
202 .inner
203 .to_pem(CsrLineEnding::LF)
204 .map_err(|e| CsrError::DerEncode {
205 context: "certificate request (PEM)",
206 source: e,
207 })?;
208 Ok(pem)
209 }
210
211 pub fn to_base64(&self) -> Result<String, CsrError> {
212 let der_bytes = self.to_der()?;
213 Ok(Base64::encode_string(&der_bytes))
214 }
215
216 pub fn to_pem_base64(&self) -> Result<String, CsrError> {
217 let pem = self.to_pem()?;
218 Ok(Base64::encode_string(pem.as_bytes()))
219 }
220
221 pub fn subject_string(&self) -> String {
222 self.inner.info.subject.to_string()
223 }
224
225 pub fn extension_values_der(&self) -> Vec<Vec<u8>> {
226 self.inner
227 .info
228 .attributes
229 .iter()
230 .flat_map(|attr| attr.values.iter())
231 .filter_map(|value| value.to_der().ok())
232 .collect()
233 }
234
235 #[allow(dead_code)]
236 pub(crate) fn inner(&self) -> &CertReq {
237 &self.inner
238 }
239}
240
241#[allow(dead_code)]
256#[derive(Validate, Debug, Clone, PartialEq, Eq, Hash)]
257#[validate_error(CsrError)]
258#[validate(non_empty, no_special_chars)]
259pub struct CsrProperties {
260 common_name: String,
261 serial_number: String,
262 organization_identifier: String,
263 organization_unit_name: String,
264 organization_name: String,
265 #[validate(is_country_code)]
266 country_name: String,
267 invoice_type: String,
268 location_address: String,
269 industry_business_category: String,
270}
271
272impl CsrProperties {
273 fn generate_subject(&self) -> Result<name::Name, CsrError> {
274 name::Name::from_str(&format!(
275 "C={},OU={},O={},CN={}",
276 &self.country_name,
277 &self.organization_unit_name,
278 &self.organization_name,
279 &self.common_name
280 ))
281 .map_err(|e| CsrError::InvalidSubject {
282 message: e.to_string(),
283 })
284 }
285
286 fn generate_template_name_extension(
287 &self,
288 env: EnvironmentType,
289 ) -> Result<TemplateNameExtension, CsrError> {
290 env.to_extension()
291 }
292
293 fn generate_san_extension(&self) -> Result<SubjectAltName, CsrError> {
294 let name = name::Name::from_str(&format!(
295 "sn={},uid={},title={},registeredAddress={},businessCategory={}",
296 &self.serial_number,
297 &self.organization_identifier,
298 &self.invoice_type,
299 &self.location_address,
300 &self.industry_business_category
301 ))
302 .map_err(|e| CsrError::InvalidSan {
303 message: e.to_string(),
304 })?;
305 let dir_name = GeneralName::DirectoryName(name);
306 Ok(SubjectAltName::from(vec![dir_name]))
307 }
308
309 pub fn build(&self, signer: &SigningKey, env: EnvironmentType) -> Result<Csr, CsrError> {
314 let subject = self.generate_subject()?;
315 let asn1_extension = self.generate_template_name_extension(env)?;
316 let san_extension = self.generate_san_extension()?;
317
318 let mut csr_builder = RequestBuilder::new(subject).map_err(|e| CsrError::RequestBuild {
319 message: e.to_string(),
320 })?;
321 csr_builder
322 .add_extension(&asn1_extension)
323 .map_err(|e| CsrError::AddExtension {
324 which: "TemplateName",
325 message: e.to_string(),
326 })?;
327 csr_builder
328 .add_extension(&san_extension)
329 .map_err(|e| CsrError::AddExtension {
330 which: "SubjectAltName",
331 message: e.to_string(),
332 })?;
333 csr_builder
334 .build::<_, ecdsa::der::Signature<_>>(signer.inner())
335 .map_err(|e| CsrError::CsrBuild {
336 message: e.to_string(),
337 })
338 .map(|inner| Csr { inner })
339 }
340
341 pub fn from_properties_str(properties: &str) -> Result<CsrProperties, CsrError> {
346 let pathbuf = path::PathBuf::from("<properties>");
347 let cursor = Cursor::new(properties.as_bytes());
348 let dst_map = read(cursor).map_err(|e| CsrError::PropertiesRead {
349 path: pathbuf.clone(),
350 source: e,
351 })?;
352
353 let req = |key: &str| -> Result<String, CsrError> {
354 dst_map
355 .get(key)
356 .map(|s| s.to_string())
357 .ok_or_else(|| CsrError::MissingProperty {
358 path: pathbuf.clone(),
359 key: key.to_string(),
360 })
361 };
362
363 let csr = CsrProperties::new(
364 req("csr.common.name")?,
365 req("csr.serial.number")?,
366 req("csr.organization.identifier")?,
367 req("csr.organization.unit.name")?,
368 req("csr.organization.name")?,
369 req("csr.country.name")?,
370 req("csr.invoice.type")?,
371 req("csr.location.address")?,
372 req("csr.industry.business.category")?,
373 )?;
374
375 Ok(csr)
376 }
377
378 pub fn parse_csr_config(properties: &str) -> Result<CsrProperties, CsrError> {
383 Self::from_properties_str(properties)
384 }
385
386 pub fn parse_csr_config_file(csr_path: impl AsRef<path::Path>) -> Result<CsrProperties, CsrError> {
391 let path = csr_path.as_ref();
392 let pathbuf = path.to_path_buf();
393 let contents = std::fs::read_to_string(path).map_err(|e| CsrError::Io {
394 path: pathbuf.clone(),
395 source: e,
396 })?;
397 let csr = CsrProperties::from_properties_str(&contents)?;
398 Ok(csr)
399 }
400}
401
402impl From<String> for CsrError {
403 fn from(message: String) -> Self {
404 CsrError::Validation { message }
405 }
406}