pyrinas_cli/
certs.rs

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
20/// Default serial port for MAC
21pub const DEFAULT_MAC_PORT: &str = "/dev/tty.SLAB_USBtoUART";
22
23/// Default security tag for Pyrinas
24pub 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    /// Serde json error
47    #[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    // CA cert params
83    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    // Get the reader
110    let mut reader = BufReader::new(port.try_clone()?);
111
112    // Set the ca certificate..
113    // AT%CMNG=0,16842753,0,""
114    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        // Flush output
126        let _ = port.flush();
127
128        // Wait for "OK" response
129        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        // Delay
137        thread::sleep(time::Duration::from_secs(2));
138    }
139
140    // Write the public key
141    if let Some(pub_key) = &cert.pub_key {
142        // AT%CMNG=0,16842753,1,""
143        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        // Flush output
154        let _ = port.flush();
155
156        // Wait for "OK" response
157        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        // Delay
165        thread::sleep(time::Duration::from_secs(2));
166    }
167
168    // AT%CMNG=0,16842753,2,""
169    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            //
175            return Err(Error::CustomError(format!(
176                " Unable to write private key. Error: {}",
177                e
178            )));
179        }
180
181        // Flush output
182        let _ = port.flush();
183
184        // Wait for "OK" response
185        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
196/// Function used to process all incoming certification generation commands
197pub 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                    // Open port
210                    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                    // issue AT command to get IMEI
217                    port.write_fmt(format_args!("AT+CGSN=1?\r\n"))?;
218
219                    // Get the current timestamp
220                    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                            // See if the line contains the start dialog
232                            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            // Generate cert
250            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                    // Get path
256                    let path = get_device_cert_path(&config.cert, &id)?;
257
258                    // Read from file
259                    let file = File::open(path)?;
260                    let reader = BufReader::new(file);
261
262                    // Convert to DeviceCert
263                    serde_json::from_reader(reader)?
264                }
265            };
266
267            // if the provision flag is set provision it
268            if cmd.provision {
269                // Open port
270                let mut port = serialport::new(&cmd.port, 115_200)
271                    .timeout(time::Duration::from_millis(10))
272                    .open()?;
273
274                // confirm provision
275                if prompt_default("Ready to provision to device. Continue?", false)? {
276                    // Device default cert
277                    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                    // Other certs as necessary
288                    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    // Get the path
309    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    // Check if CA exits
314    if std::path::Path::new(&ca_der_path).exists() {
315        return Err(Error::AlreadyExists {
316            name: "ca".to_string(),
317        });
318    }
319    // CA cert params
320    let mut params: CertificateParams = get_default_params(config);
321
322    // This can sign things!
323    params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
324
325    // Set the key usage
326    params.key_usages = vec![KeyUsagePurpose::CrlSign, KeyUsagePurpose::KeyCertSign];
327
328    // Set this to 10 years instead of default 4
329    params.not_after = params
330        .not_before
331        .with_year(params.not_before.year() + 10)
332        .unwrap();
333
334    // Make sure folder exists
335    std::fs::create_dir_all(format!("{}/certs/{}/ca", config_path, config.domain))?;
336
337    // Create ca
338    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    // Serialize output
361    let cert_pem = cert.serialize_pem_with_signer(ca_cert).unwrap();
362    let key_pem = cert.serialize_private_key_pem();
363
364    // Get CA cert to pem but don't keep re-signing it..
365    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    // Export as JSON
373    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    // Make sure there's a directory
383    std::fs::create_dir_all(format!("{}/certs/{}/{}/", config_path, config.domain, name))?;
384
385    // Write JSON
386    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    // Serialize output
408    let cert_pem = cert.serialize_pem_with_signer(ca_cert).unwrap();
409    let key_pem = cert.serialize_private_key_pem();
410
411    // Create directory if not already
412    std::fs::create_dir_all(format!("{}/certs/{}/{}/", config_path, config.domain, name))?;
413
414    // Write to files
415    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    // Config path
441    let config_path = crate::config::get_config_path()?
442        .to_string_lossy()
443        .to_string();
444
445    // Path to pfx
446    let ca_pfx_path = format!(
447        "{}/certs/{}/{}/{}.pfx",
448        config_path, config.domain, name, name
449    );
450
451    // Check if it exists
452    if std::path::Path::new(&ca_pfx_path).exists() {
453        return Err(Error::AlreadyExists {
454            name: name.to_string(),
455        });
456    }
457
458    // Create directory if not already
459    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    // Serialize ca_der as bytes without re-signing..
465
466    // Generate pfx file!
467    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    // Write it
473    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    // Load CA
484    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    // Import the CA
497    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    // Return the cert or error
503    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    // Check if it exists
518    if std::path::Path::new(&server_cert_path).exists() {
519        return Err(Error::AlreadyExists { name });
520    }
521
522    // Get CA Cert
523    let (ca_cert, ca_der) = get_ca_cert(config)?;
524
525    // Cert params
526    let mut params: CertificateParams = get_default_params(config);
527
528    // Set the key usage
529    params.key_usages = vec![
530        KeyUsagePurpose::DigitalSignature,
531        KeyUsagePurpose::KeyEncipherment,
532    ];
533
534    // Set the ext key useage
535    params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
536
537    // Set the alt name
538    params.subject_alt_names = vec![SanType::DnsName(config.domain.clone())];
539
540    // Make the cert
541    let cert = Certificate::from_params(params)?;
542
543    // Write cert to file(s)
544    write_keypair_pem(config, &name, &cert, &ca_cert)?;
545
546    // Write pfx
547    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    // Check if it exists
572    if std::path::Path::new(&device_cert_path).exists() {
573        return Err(Error::AlreadyExists {
574            name: name.to_string(),
575        });
576    }
577
578    // Get CA Cert
579    let (ca_cert, ca_der) = get_ca_cert(config)?;
580
581    // Cert params
582    let mut params: CertificateParams = get_default_params(config);
583
584    // Set the key usage
585    params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
586
587    // Set the ext key useage
588    params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
589
590    // Set the alt name
591    params.subject_alt_names = vec![SanType::Rfc822Name(format!(
592        "{}@{}",
593        name,
594        config.domain.clone()
595    ))];
596
597    // Make the cert
598    let cert = Certificate::from_params(params)?;
599
600    // Write all cert info to file(s)
601    // write_keypair_pem(&config, &name, &cert, &ca_cert)?;
602
603    // Write nRF Connect Desktop compatable JSON for cert install
604    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}