1use chrono::{Datelike, Utc};
2use p12::PFX;
3use pem::Pem;
4use promptly::prompt_default;
5use rcgen::{
6 BasicConstraints, Certificate, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa,
7 KeyUsagePurpose, SanType,
8};
9use serialport::{self, SerialPort};
10use std::{
11 convert::TryInto,
12 fs::{self, File},
13 io::{self, BufRead, BufReader},
14 thread, time,
15};
16use thiserror::Error;
17
18use crate::{config, ota, CertCmd, CertConfig, CertEntry, CertSubcommand};
19
20pub const DEFAULT_MAC_PORT: &str = "/dev/tty.SLAB_USBtoUART";
22
23pub const DEFAULT_PYRINAS_SECURITY_TAG: u32 = 1234;
25
26#[derive(Debug, Error)]
27pub enum Error {
28 #[error("{source}")]
29 FileError {
30 #[from]
31 source: io::Error,
32 },
33
34 #[error("{source}")]
35 CertGen {
36 #[from]
37 source: rcgen::RcgenError,
38 },
39
40 #[error("pfx gen error")]
41 PfxGen,
42
43 #[error("cert for {name} already exists!")]
44 AlreadyExists { name: String },
45
46 #[error("{source}")]
48 JsonError {
49 #[from]
50 source: serde_json::Error,
51 },
52
53 #[error("{source}")]
54 ConfigError {
55 #[from]
56 source: config::Error,
57 },
58
59 #[error("{source}")]
60 OtaError {
61 #[from]
62 source: ota::Error,
63 },
64
65 #[error("{source}")]
66 SerialError {
67 #[from]
68 source: serialport::Error,
69 },
70
71 #[error("{source}")]
72 PromptError {
73 #[from]
74 source: promptly::ReadlineError,
75 },
76
77 #[error("err: {0}")]
78 CustomError(String),
79}
80
81fn get_default_params(config: &crate::CertConfig) -> CertificateParams {
82 let mut params: CertificateParams = Default::default();
84
85 params.not_before = Utc::now();
86 params.not_after = params
87 .not_before
88 .with_year(params.not_before.year() + 4)
89 .unwrap();
90
91 params
92 .distinguished_name
93 .push(DnType::CountryName, config.country.clone());
94 params
95 .distinguished_name
96 .push(DnType::OrganizationName, config.organization.clone());
97 params
98 .distinguished_name
99 .push(DnType::CommonName, config.domain.clone());
100
101 params.subject_alt_names = vec![SanType::DnsName(config.domain.clone())];
102
103 params.use_authority_key_identifier_extension = true;
104
105 params
106}
107
108fn write_credential(port: &mut Box<dyn SerialPort>, cert: &CertEntry) -> Result<(), Error> {
109 let mut reader = BufReader::new(port.try_clone()?);
111
112 if let Some(ca_cert) = &cert.ca_cert {
115 if let Err(e) = port.write_fmt(format_args!(
116 "AT%CMNG=0,{},0,\"{}\"\r\n",
117 &cert.tag, &ca_cert
118 )) {
119 return Err(Error::CustomError(format!(
120 "Unable to write CA cert. Error: {}",
121 e
122 )));
123 }
124
125 let _ = port.flush();
127
128 loop {
130 let mut line = String::new();
131 if reader.read_line(&mut line).is_ok() && line.contains("OK") {
132 break;
133 }
134 }
135
136 thread::sleep(time::Duration::from_secs(2));
138 }
139
140 if let Some(pub_key) = &cert.pub_key {
142 if let Err(e) = port.write_fmt(format_args!(
144 "AT%CMNG=0,{},1,\"{}\"\r\n",
145 &cert.tag, pub_key
146 )) {
147 return Err(Error::CustomError(format!(
148 "Unable to write client cert. Error: {}",
149 e
150 )));
151 }
152
153 let _ = port.flush();
155
156 loop {
158 let mut line = String::new();
159 if reader.read_line(&mut line).is_ok() && line.contains("OK") {
160 break;
161 }
162 }
163
164 thread::sleep(time::Duration::from_secs(2));
166 }
167
168 if let Some(private_key) = &cert.private_key {
170 if let Err(e) = port.write_fmt(format_args!(
171 "AT%CMNG=0,{},2,\"{}\"\r\n",
172 cert.tag, private_key
173 )) {
174 return Err(Error::CustomError(format!(
176 " Unable to write private key. Error: {}",
177 e
178 )));
179 }
180
181 let _ = port.flush();
183
184 loop {
186 let mut line = String::new();
187 if reader.read_line(&mut line).is_ok() && line.contains("OK") {
188 break;
189 }
190 }
191 }
192
193 Ok(())
194}
195
196pub fn process(config: &crate::Config, c: &CertCmd) -> Result<(), Error> {
198 match &c.subcmd {
199 CertSubcommand::Ca => {
200 generate_ca_cert(&config.cert)?;
201 }
202 CertSubcommand::Server => {
203 generate_server_cert(&config.cert)?;
204 }
205 CertSubcommand::Device(cmd) => {
206 let id = match cmd.id.clone() {
207 Some(id) => id,
208 None => {
209 let mut port = serialport::new(&cmd.port, 115_200)
211 .timeout(time::Duration::from_millis(10))
212 .open()?;
213
214 let mut reader = BufReader::new(port.try_clone()?);
215
216 port.write_fmt(format_args!("AT+CGSN=1?\r\n"))?;
218
219 let now = time::Instant::now();
221
222 loop {
223 if now.elapsed().as_secs() > 5 {
224 return Err(Error::CustomError(String::from(
225 "Timeout communicating with device.",
226 )));
227 }
228
229 let mut line = String::new();
230 if reader.read_line(&mut line).is_ok() {
231 if line.contains("+CGSN: ") {
233 break line
234 .strip_prefix("+CGSN: ")
235 .expect("Should have had a value!")
236 .trim_end()
237 .trim_matches('\"')
238 .to_string();
239 } else if line.contains("at_host: Error") {
240 return Err(Error::CustomError(String::from(
241 "AT error communicating with device.",
242 )));
243 }
244 }
245 }
246 }
247 };
248
249 let certs = match generate_device_cert(&config.cert, &id) {
251 Ok(c) => c,
252 Err(_e) => {
253 println!("Cert for {} already generated!", &id);
254
255 let path = get_device_cert_path(&config.cert, &id)?;
257
258 let file = File::open(path)?;
260 let reader = BufReader::new(file);
261
262 serde_json::from_reader(reader)?
264 }
265 };
266
267 if cmd.provision {
269 let mut port = serialport::new(&cmd.port, 115_200)
271 .timeout(time::Duration::from_millis(10))
272 .open()?;
273
274 if prompt_default("Ready to provision to device. Continue?", false)? {
276 write_credential(
278 &mut port,
279 &CertEntry {
280 tag: cmd.tag.unwrap_or(DEFAULT_PYRINAS_SECURITY_TAG),
281 ca_cert: Some(certs.ca_cert),
282 private_key: Some(certs.private_key),
283 pub_key: Some(certs.client_cert),
284 },
285 )?;
286
287 if let Some(alts) = &config.alts {
289 for entry in alts {
290 write_credential(&mut port, entry)?;
291 }
292 }
293
294 println!("Provisioning complete!");
295 }
296 }
297 }
298 };
299
300 Ok(())
301}
302
303pub fn generate_ca_cert(config: &crate::CertConfig) -> Result<(), Error> {
304 let config_path = crate::config::get_config_path()?
305 .to_string_lossy()
306 .to_string();
307
308 let ca_der_path = format!("{}/certs/{}/ca/ca.der", config_path, config.domain);
310 let ca_private_der_path = format!("{}/certs/{}/ca/ca.key.der", config_path, config.domain);
311 let ca_pem_path = format!("{}/certs/{}/ca/ca.pem", config_path, config.domain);
312
313 if std::path::Path::new(&ca_der_path).exists() {
315 return Err(Error::AlreadyExists {
316 name: "ca".to_string(),
317 });
318 }
319 let mut params: CertificateParams = get_default_params(config);
321
322 params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
324
325 params.key_usages = vec![KeyUsagePurpose::CrlSign, KeyUsagePurpose::KeyCertSign];
327
328 params.not_after = params
330 .not_before
331 .with_year(params.not_before.year() + 10)
332 .unwrap();
333
334 std::fs::create_dir_all(format!("{}/certs/{}/ca", config_path, config.domain))?;
336
337 let ca_cert = Certificate::from_params(params).unwrap();
339
340 fs::write(ca_der_path, &ca_cert.serialize_der().unwrap())?;
341 fs::write(ca_private_der_path, &ca_cert.serialize_private_key_der())?;
342 fs::write(ca_pem_path, &ca_cert.serialize_pem().unwrap())?;
343
344 println!("Exported CA to {}", config_path);
345
346 Ok(())
347}
348
349fn write_device_json(
350 config: &CertConfig,
351 name: &str,
352 cert: &Certificate,
353 ca_cert: &Certificate,
354 ca_der: &[u8],
355) -> Result<crate::device::DeviceCert, Error> {
356 let config_path = crate::config::get_config_path()?
357 .to_string_lossy()
358 .to_string();
359
360 let cert_pem = cert.serialize_pem_with_signer(ca_cert).unwrap();
362 let key_pem = cert.serialize_private_key_pem();
363
364 let p = Pem {
366 tag: "CERTIFICATE".to_string(),
367 contents: ca_der.to_vec(),
368 };
369
370 let ca_pem = pem::encode(&p);
371
372 let json_device_cert = crate::device::DeviceCert {
374 private_key: key_pem,
375 client_cert: cert_pem,
376 ca_cert: ca_pem,
377 client_id: name.to_string(),
378 };
379
380 let json_output = serde_json::to_string(&json_device_cert)?;
381
382 std::fs::create_dir_all(format!("{}/certs/{}/{}/", config_path, config.domain, name))?;
384
385 fs::write(
387 format!(
388 "{}/certs/{}/{}/{}.json",
389 config_path, config.domain, name, name
390 ),
391 &json_output.as_bytes(),
392 )?;
393
394 Ok(json_device_cert)
395}
396
397pub fn write_keypair_pem(
398 config: &CertConfig,
399 name: &str,
400 cert: &Certificate,
401 ca_cert: &Certificate,
402) -> Result<(), Error> {
403 let config_path = crate::config::get_config_path()?
404 .to_string_lossy()
405 .to_string();
406
407 let cert_pem = cert.serialize_pem_with_signer(ca_cert).unwrap();
409 let key_pem = cert.serialize_private_key_pem();
410
411 std::fs::create_dir_all(format!("{}/certs/{}/{}/", config_path, config.domain, name))?;
413
414 fs::write(
416 format!(
417 "{}/certs/{}/{}/{}.pem",
418 config_path, config.domain, name, name
419 ),
420 &cert_pem.as_bytes(),
421 )?;
422 fs::write(
423 format!(
424 "{}/certs/{}/{}/{}.key",
425 config_path, config.domain, name, name
426 ),
427 &key_pem.as_bytes(),
428 )?;
429
430 Ok(())
431}
432
433fn write_pfx(
434 config: &CertConfig,
435 name: &str,
436 cert: &Certificate,
437 ca_cert: &Certificate,
438 ca_der: &[u8],
439) -> Result<(), Error> {
440 let config_path = crate::config::get_config_path()?
442 .to_string_lossy()
443 .to_string();
444
445 let ca_pfx_path = format!(
447 "{}/certs/{}/{}/{}.pfx",
448 config_path, config.domain, name, name
449 );
450
451 if std::path::Path::new(&ca_pfx_path).exists() {
453 return Err(Error::AlreadyExists {
454 name: name.to_string(),
455 });
456 }
457
458 std::fs::create_dir_all(format!("{}/certs/{}/{}/", config_path, config.domain, name))?;
460
461 let cert_der = cert.serialize_der_with_signer(ca_cert)?;
462 let key_der = cert.serialize_private_key_der();
463
464 let ca_pfx = PFX::new(&cert_der, &key_der, Some(ca_der), &config.pfx_pass, name)
468 .ok_or(Error::PfxGen)?
469 .to_der()
470 .to_vec();
471
472 fs::write(ca_pfx_path, ca_pfx)?;
474
475 Ok(())
476}
477
478pub fn get_ca_cert(config: &crate::CertConfig) -> Result<(Certificate, Vec<u8>), Error> {
479 let config_path = crate::config::get_config_path()?
480 .to_string_lossy()
481 .to_string();
482
483 let path = format!("{}/certs/{}/ca/ca.der", config_path, config.domain);
485 let ca_cert_der = match fs::read(path.clone()) {
486 Ok(d) => d,
487 Err(_) => panic!("{} not found. Generate CA first!", path),
488 };
489
490 let path = format!("{}/certs/{}/ca/ca.key.der", config_path, config.domain);
491 let ca_cert_key_der = match fs::read(path.clone()) {
492 Ok(d) => d,
493 Err(_) => panic!("{} not found. Generate CA first!", path),
494 };
495
496 let ca_cert_params = CertificateParams::from_ca_cert_der(
498 ca_cert_der.as_slice(),
499 ca_cert_key_der.as_slice().try_into()?,
500 )?;
501
502 Ok((Certificate::from_params(ca_cert_params)?, ca_cert_der))
504}
505
506pub fn generate_server_cert(config: &crate::CertConfig) -> Result<(), Error> {
507 let config_path = crate::config::get_config_path()?
508 .to_string_lossy()
509 .to_string();
510 let name = "server".to_string();
511
512 let server_cert_path = format!(
513 "{}/certs/{}/{}/{}.pfx",
514 config_path, config.domain, name, name
515 );
516
517 if std::path::Path::new(&server_cert_path).exists() {
519 return Err(Error::AlreadyExists { name });
520 }
521
522 let (ca_cert, ca_der) = get_ca_cert(config)?;
524
525 let mut params: CertificateParams = get_default_params(config);
527
528 params.key_usages = vec![
530 KeyUsagePurpose::DigitalSignature,
531 KeyUsagePurpose::KeyEncipherment,
532 ];
533
534 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
536
537 params.subject_alt_names = vec![SanType::DnsName(config.domain.clone())];
539
540 let cert = Certificate::from_params(params)?;
542
543 write_keypair_pem(config, &name, &cert, &ca_cert)?;
545
546 write_pfx(config, &name, &cert, &ca_cert, &ca_der)?;
548
549 println!("Exported server .pfx to {}", config_path);
550
551 Ok(())
552}
553
554pub fn get_device_cert_path(config: &crate::CertConfig, name: &str) -> Result<String, Error> {
555 let config_path = crate::config::get_config_path()?
556 .to_string_lossy()
557 .to_string();
558
559 Ok(format!(
560 "{}/certs/{}/{}/{}.pem",
561 config_path, config.domain, name, name
562 ))
563}
564
565pub fn generate_device_cert(
566 config: &crate::CertConfig,
567 name: &str,
568) -> Result<crate::device::DeviceCert, Error> {
569 let device_cert_path = get_device_cert_path(config, name)?;
570
571 if std::path::Path::new(&device_cert_path).exists() {
573 return Err(Error::AlreadyExists {
574 name: name.to_string(),
575 });
576 }
577
578 let (ca_cert, ca_der) = get_ca_cert(config)?;
580
581 let mut params: CertificateParams = get_default_params(config);
583
584 params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
586
587 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
589
590 params.subject_alt_names = vec![SanType::Rfc822Name(format!(
592 "{}@{}",
593 name,
594 config.domain.clone()
595 ))];
596
597 let cert = Certificate::from_params(params)?;
599
600 let certs = write_device_json(config, name, &cert, &ca_cert, &ca_der)?;
605
606 println!("Exported cert for {} to {}", name, device_cert_path);
607
608 Ok(certs)
609}