Skip to main content

opcua_crypto/
certificate_store.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2024 Adam Lock
4
5//! The certificate store holds and retrieves private keys and certificates from disk. It is responsible
6//! for checking certificates supplied by the remote end to see if they are valid and trusted or not.
7
8use std::fs::File;
9use std::io::{Read, Write};
10use std::path::{Path, PathBuf};
11
12use tracing::{debug, error, info, trace, warn};
13
14use opcua_types::status_code::StatusCode;
15
16use crate::PrivateKey;
17
18use super::{
19    security_policy::SecurityPolicy,
20    x509::{X509Data, X509},
21};
22
23/// Default path to the applications own certificate
24const OWN_CERTIFICATE_PATH: &str = "own/cert.der";
25/// Default path to the applications own private key
26const OWN_PRIVATE_KEY_PATH: &str = "private/private.pem";
27/// The directory holding trusted certificates
28const TRUSTED_CERTS_DIR: &str = "trusted";
29/// The directory holding rejected certificates
30const REJECTED_CERTS_DIR: &str = "rejected";
31
32/// The certificate store manages the storage of a server/client's own certificate & private key
33/// and the trust / rejection of certificates from the other end.
34pub struct CertificateStore {
35    /// Path to the applications own certificate
36    own_certificate_path: PathBuf,
37    /// Path to the applications own private key
38    own_private_key_path: PathBuf,
39    /// Path to the certificate store on disk
40    pub(crate) pki_path: PathBuf,
41    /// Timestamps of the cert are normally checked on the cert to ensure it cannot be used before
42    /// or after its limits, but this check can be disabled.
43    check_time: bool,
44    /// This option lets you skip additional certificate validations (e.g. hostname, application
45    /// uri and the not before / after values). Certificates are always checked to see if they are
46    /// trusted and have a valid key length.
47    skip_verify_certs: bool,
48    /// Ordinarily an unknown cert will be dropped into the rejected folder, but it can be dropped
49    /// into the trusted folder if this flag is set. Certs in the trusted folder must still pass
50    /// validity checks.
51    trust_unknown_certs: bool,
52}
53
54impl CertificateStore {
55    /// Sets up the certificate store to the specified PKI directory.
56    /// It is a bad idea to have more than one running instance pointing to the same path
57    /// location on disk.
58    pub fn new(pki_path: &Path) -> CertificateStore {
59        CertificateStore {
60            own_certificate_path: PathBuf::from(OWN_CERTIFICATE_PATH),
61            own_private_key_path: PathBuf::from(OWN_PRIVATE_KEY_PATH),
62            pki_path: pki_path.to_path_buf(),
63            check_time: true,
64            skip_verify_certs: false,
65            trust_unknown_certs: false,
66        }
67    }
68
69    /// Create a new certificate store with application certificate from the given
70    /// `cert_path`.
71    pub fn new_with_x509_data<X>(
72        pki_path: &Path,
73        overwrite: bool,
74        cert_path: Option<&Path>,
75        pkey_path: Option<&Path>,
76        x509_data: Option<X>,
77    ) -> (CertificateStore, Option<X509>, Option<PrivateKey>)
78    where
79        X: Into<X509Data>,
80    {
81        let mut certificate_store = CertificateStore::new(pki_path);
82        if let (Some(cert_path), Some(pkey_path)) = (cert_path, pkey_path) {
83            certificate_store.own_certificate_path = cert_path.to_path_buf();
84            certificate_store.own_private_key_path = pkey_path.to_path_buf();
85        }
86        let (cert, pkey) = if certificate_store.ensure_pki_path().is_err() {
87            error!("Folder for storing certificates cannot be examined so server has no application instance certificate or private key.");
88            (None, None)
89        } else {
90            let cert = certificate_store.read_own_cert();
91            let pkey = certificate_store.read_own_pkey();
92            match (cert, pkey, x509_data) {
93                (Ok(cert), Ok(pkey), _) => (Some(cert), Some(pkey)),
94                (_, _, Some(x509_data)) => {
95                    info!("Creating sample application instance certificate and private key");
96                    let x509_data = x509_data.into();
97                    let result = certificate_store
98                        .create_and_store_application_instance_cert(&x509_data, overwrite);
99                    match result {
100                        Ok((cert, pkey)) => (Some(cert), Some(pkey)),
101                        Err(err) => {
102                            error!("Certificate creation failed, error = {}", err);
103                            (None, None)
104                        }
105                    }
106                }
107                (Err(e1), Err(e2), _) => {
108                    error!("Failed to get cert and private key: {e1}, {e2}");
109                    (None, None)
110                }
111                (Err(e), _, _) | (_, Err(e), _) => {
112                    error!("Failed to get cert or private key: {e}");
113                    (None, None)
114                }
115            }
116        };
117        (certificate_store, cert, pkey)
118    }
119
120    /// Set `skip_verify_certs` to not verify incoming certificates.
121    pub fn set_skip_verify_certs(&mut self, skip_verify_certs: bool) {
122        self.skip_verify_certs = skip_verify_certs;
123    }
124
125    /// Set `trust_unknown_certs` to automatically trust valid but
126    /// untrusted certificates.
127    pub fn set_trust_unknown_certs(&mut self, trust_unknown_certs: bool) {
128        self.trust_unknown_certs = trust_unknown_certs;
129    }
130
131    /// Check expiration time of incoming certificates.
132    pub fn set_check_time(&mut self, check_time: bool) {
133        self.check_time = check_time;
134    }
135
136    /// Reads a private key from a path on disk.
137    pub fn read_pkey(path: &Path) -> Result<PrivateKey, String> {
138        if let Ok(pkey) = PrivateKey::read_pem_file(path) {
139            return Ok(pkey);
140        }
141
142        Err(format!("Cannot read pkey from path {path:?}"))
143    }
144
145    /// Reads the store's own certificate
146    pub fn read_own_cert(&self) -> Result<X509, String> {
147        CertificateStore::read_cert(&self.own_certificate_path()).map_err(|e| {
148            format!(
149                "Cannot read cert from path {:?}: {e}",
150                self.own_certificate_path()
151            )
152        })
153    }
154
155    /// Read own private key from file.
156    pub fn read_own_pkey(&self) -> Result<PrivateKey, String> {
157        CertificateStore::read_pkey(&self.own_private_key_path()).map_err(|e| {
158            format!(
159                "Cannot read pkey from path {:?}: {e}",
160                self.own_private_key_path()
161            )
162        })
163    }
164
165    /// Create a certificate and key pair to the specified locations
166    pub fn create_certificate_and_key(
167        args: &X509Data,
168        overwrite: bool,
169        cert_path: &Path,
170        pkey_path: &Path,
171    ) -> Result<(X509, PrivateKey), String> {
172        let (cert, pkey) = X509::cert_and_pkey(args)?;
173
174        // Write the public cert
175        let _ = CertificateStore::store_cert(&cert, cert_path, overwrite)?;
176
177        // Write the private key
178        use rsa::pkcs8;
179        use x509_cert::der::pem::PemLabel;
180        let doc = pkey.to_der().unwrap();
181        let pem = doc
182            .to_pem(rsa::pkcs8::PrivateKeyInfo::PEM_LABEL, pkcs8::LineEnding::CR)
183            .unwrap();
184        let _ = CertificateStore::write_to_file(pem.as_bytes(), pkey_path, overwrite)?;
185        Ok((cert, pkey))
186    }
187
188    /// This function will use the supplied arguments to create an Application Instance Certificate
189    /// consisting of a X509v3 certificate and public/private key pair. The cert (including pubkey)
190    /// and private key will be written to disk under the pki path.
191    pub fn create_and_store_application_instance_cert(
192        &self,
193        args: &X509Data,
194        overwrite: bool,
195    ) -> Result<(X509, PrivateKey), String> {
196        CertificateStore::create_certificate_and_key(
197            args,
198            overwrite,
199            &self.own_certificate_path(),
200            &self.own_private_key_path(),
201        )
202    }
203
204    /// Validates the cert as trusted and valid. If the cert is unknown, it will be written to
205    /// the rejected folder so that the administrator can manually move it to the trusted folder.
206    ///
207    /// # Errors
208    ///
209    /// A non `Good` status code indicates a failure in the cert or in some action required in
210    /// order to validate it.
211    ///
212    pub fn validate_or_reject_application_instance_cert(
213        &self,
214        cert: &X509,
215        security_policy: SecurityPolicy,
216        hostname: Option<&str>,
217        application_uri: Option<&str>,
218    ) -> Result<(), StatusCode> {
219        self.validate_application_instance_cert(cert, security_policy, hostname, application_uri)
220    }
221
222    /// Ensures that the cert provided is the same as the one specified by a path. This is a
223    /// security check to stop someone from renaming a cert on disk to match another cert and
224    /// somehow bypassing or subverting a check. The disk cert must exactly match the memory cert
225    /// or the test is assumed to fail.
226    fn ensure_cert_and_file_are_the_same(cert: &X509, cert_path: &Path) -> bool {
227        if !cert_path.exists() {
228            trace!("Cannot find cert on disk");
229            false
230        } else {
231            match CertificateStore::read_cert(cert_path) {
232                Ok(file_der) => {
233                    // Compare the buffers
234                    trace!("Comparing cert on disk to memory");
235                    let der;
236                    {
237                        let r = cert.to_der();
238                        match r {
239                            Err(_) => return false,
240                            Ok(val) => der = val,
241                        }
242                    }
243
244                    let target_der;
245                    {
246                        let r = file_der.to_der();
247                        match r {
248                            Err(_) => return false,
249                            Ok(val) => target_der = val,
250                        }
251                    }
252
253                    der == target_der
254                }
255                Err(err) => {
256                    trace!("Cannot read cert from disk {:?} - {}", cert_path, err);
257                    // No cert2 to compare to
258                    false
259                }
260            }
261        }
262    }
263
264    /// Validates the certificate according to the strictness set in the CertificateStore itself.
265    /// Validation might include checking the issue time, expiration time, revocation, trust chain
266    /// etc. In the first instance this function will only check if the cert is recognized
267    /// and is already contained in the trusted or rejected folder.
268    ///
269    /// # Errors
270    ///
271    /// A non `Good` status code indicates a failure in the cert or in some action required in
272    /// order to validate it.
273    ///
274    pub fn validate_application_instance_cert(
275        &self,
276        cert: &X509,
277        security_policy: SecurityPolicy,
278        hostname: Option<&str>,
279        application_uri: Option<&str>,
280    ) -> Result<(), StatusCode> {
281        let cert_file_name = CertificateStore::cert_file_name(cert);
282        debug!("Validating cert with name on disk {}", cert_file_name);
283
284        // Look for the cert in the rejected folder. If it's rejected there is no purpose going
285        // any further
286        {
287            let mut cert_path = self.rejected_certs_dir();
288            if !cert_path.exists() {
289                error!(
290                    "Path for rejected certificates {} does not exist",
291                    cert_path.display()
292                );
293                return Err(StatusCode::BadUnexpectedError);
294            }
295            cert_path.push(&cert_file_name);
296            if cert_path.exists() {
297                warn!(
298                    "Certificate {} is untrusted because it resides in the rejected directory",
299                    cert_file_name
300                );
301                return Err(StatusCode::BadSecurityChecksFailed);
302            }
303        }
304
305        // Check the trusted folder. These checks are more strict to ensure the cert is genuinely
306        // trusted
307        {
308            // Check the trusted folder
309            let mut cert_path = self.trusted_certs_dir();
310            if !cert_path.exists() {
311                error!(
312                    "Path for rejected certificates {} does not exist",
313                    cert_path.display()
314                );
315                return Err(StatusCode::BadUnexpectedError);
316            }
317            cert_path.push(&cert_file_name);
318
319            // Check if cert is in the trusted folder
320            if !cert_path.exists() {
321                // ... trust checks based on ca could be added here to add cert straight to trust folder
322                if self.trust_unknown_certs {
323                    // Put the unknown cert into the trusted folder
324                    warn!("Certificate {} is unknown but policy will store it into the trusted directory", cert_file_name);
325                    let _ = self.store_trusted_cert(cert);
326                // Note that we drop through and still check the cert for validity
327                } else {
328                    warn!("Certificate {} is unknown and untrusted so it will be stored in rejected directory", cert_file_name);
329                    let _ = self.store_rejected_cert(cert);
330                    return Err(StatusCode::BadCertificateUntrusted);
331                }
332            }
333
334            // Read the cert from the trusted folder to make sure it matches the one supplied
335            if !CertificateStore::ensure_cert_and_file_are_the_same(cert, &cert_path) {
336                error!("Certificate in memory does not match the one on disk {} so cert will automatically be treated as untrusted", cert_path.display());
337                return Err(StatusCode::BadUnexpectedError);
338            }
339
340            // Check that the certificate is the right length for the security policy
341            match cert.key_length() {
342                Err(_) => {
343                    error!("Cannot read key length from certificate {}", cert_file_name);
344                    return Err(StatusCode::BadSecurityChecksFailed);
345                }
346                Ok(key_length) => {
347                    if !security_policy.is_valid_keylength(key_length) {
348                        warn!(
349                            "Certificate {} has an invalid key length {} for the policy {}",
350                            cert_file_name, key_length, security_policy
351                        );
352                        return Err(StatusCode::BadSecurityChecksFailed);
353                    }
354                }
355            }
356
357            if self.skip_verify_certs {
358                debug!(
359                    "Skipping additional verifications for certificate {}",
360                    cert_file_name
361                );
362                return Ok(());
363            }
364
365            // Now inspect the cert not before / after values to ensure its validity
366            if self.check_time {
367                use chrono::Utc;
368                let now = Utc::now();
369                cert.is_time_valid(&now)?;
370            }
371
372            // Compare the hostname of the cert against the cert supplied
373            if let Some(hostname) = hostname {
374                cert.is_hostname_valid(hostname)?;
375            }
376
377            // Compare the application / product uri to the supplied application description
378            if let Some(application_uri) = application_uri {
379                cert.is_application_uri_valid(application_uri)?;
380            }
381
382            // Other tests that we might do with trust lists
383            // ... issuer
384            // ... trust (self-signed, ca etc.)
385            // ... revocation
386        }
387        Ok(())
388    }
389
390    /// Returns a certificate file name from the cert's issuer and thumbprint fields.
391    /// File name is either "prefix - \[thumbprint\].der" or "thumbprint.der" depending on
392    /// the cert's common name being empty or not
393    pub fn cert_file_name(cert: &X509) -> String {
394        let prefix = if let Ok(common_name) = cert.common_name() {
395            common_name.trim().to_string().replace('/', "")
396        } else {
397            String::new()
398        };
399        let thumbprint = cert.thumbprint().as_hex_string();
400
401        if !prefix.is_empty() {
402            format!("{prefix} [{thumbprint}].der")
403        } else {
404            format!("{thumbprint}.der")
405        }
406    }
407
408    /// Creates the PKI directory structure
409    ///
410    /// # Errors
411    ///
412    /// A string description of any failure
413    ///
414    pub fn ensure_pki_path(&self) -> Result<(), String> {
415        let mut path = self.pki_path.clone();
416        let subdirs = [TRUSTED_CERTS_DIR, REJECTED_CERTS_DIR];
417        for subdir in &subdirs {
418            path.push(subdir);
419            CertificateStore::ensure_dir(&path)?;
420            path.pop();
421        }
422        Ok(())
423    }
424
425    /// Ensure the directory exists, creating it if necessary
426    ///
427    /// # Errors
428    ///
429    /// A string description of any failure
430    ///
431    fn ensure_dir(path: &Path) -> Result<(), String> {
432        if path.exists() {
433            if !path.is_dir() {
434                Err(format!("{} is not a directory ", path.display()))
435            } else {
436                Ok(())
437            }
438        } else {
439            std::fs::create_dir_all(path)
440                .map_err(|_| format!("Cannot make directories for {}", path.display()))
441        }
442    }
443
444    /// Get path to application instance certificate
445    pub fn own_certificate_path(&self) -> PathBuf {
446        let mut path = PathBuf::from(&self.pki_path);
447        path.push(&self.own_certificate_path);
448        path
449    }
450
451    /// Get path to application instance private key
452    pub fn own_private_key_path(&self) -> PathBuf {
453        let mut path = PathBuf::from(&self.pki_path);
454        path.push(&self.own_private_key_path);
455        path
456    }
457
458    /// Get the path to the rejected certs dir
459    pub fn rejected_certs_dir(&self) -> PathBuf {
460        let mut path = PathBuf::from(&self.pki_path);
461        path.push(REJECTED_CERTS_DIR);
462        path
463    }
464
465    /// Get the path to the trusted certs dir
466    pub fn trusted_certs_dir(&self) -> PathBuf {
467        let mut path = PathBuf::from(&self.pki_path);
468        path.push(TRUSTED_CERTS_DIR);
469        path
470    }
471
472    /// Write a cert to the rejected directory. If the write succeeds, the function
473    /// returns a path to the written file.
474    ///
475    /// # Errors
476    ///
477    /// A string description of any failure
478    ///
479    pub fn store_rejected_cert(&self, cert: &X509) -> Result<PathBuf, String> {
480        // Store the cert in the rejected folder where untrusted certs go
481        let cert_file_name = CertificateStore::cert_file_name(cert);
482        let mut cert_path = self.rejected_certs_dir();
483        cert_path.push(&cert_file_name);
484        let _ = CertificateStore::store_cert(cert, &cert_path, true)?;
485        Ok(cert_path)
486    }
487
488    /// Writes a cert to the trusted directory. If the write succeeds, the function
489    /// returns a path to the written file.
490    ///
491    /// # Errors
492    ///
493    /// A string description of any failure
494    ///
495    fn store_trusted_cert(&self, cert: &X509) -> Result<PathBuf, String> {
496        // Store the cert in the trusted folder where trusted certs go
497        let cert_file_name = CertificateStore::cert_file_name(cert);
498        let mut cert_path = self.trusted_certs_dir();
499        cert_path.push(&cert_file_name);
500        let _ = CertificateStore::store_cert(cert, &cert_path, true)?;
501        Ok(cert_path)
502    }
503
504    /// Writes a cert to the specified directory
505    ///
506    /// # Errors
507    ///
508    /// A string description of any failure
509    ///
510    fn store_cert(cert: &X509, path: &Path, overwrite: bool) -> Result<usize, String> {
511        let der = cert.to_der().unwrap();
512        info!("Writing X509 cert to {}", path.display());
513        CertificateStore::write_to_file(&der, path, overwrite)
514    }
515
516    /// Reads an X509 certificate in .def or .pem format from disk
517    ///
518    /// # Errors
519    ///
520    /// A string description of any failure
521    ///
522    pub fn read_cert(path: &Path) -> Result<X509, String> {
523        let file = File::open(path);
524        if file.is_err() {
525            return Err(format!("Could not open cert file {}", path.display()));
526        }
527
528        let mut file: File = file.unwrap();
529        let mut cert = Vec::new();
530        let bytes_read = file.read_to_end(&mut cert);
531        if bytes_read.is_err() {
532            return Err(format!(
533                "Could not read bytes from cert file {}",
534                path.display()
535            ));
536        }
537
538        let cert = match path.extension() {
539            Some(v) if v == "der" => X509::from_der(&cert),
540            Some(v) if v == "pem" => X509::from_pem(&cert),
541            _ => return Err("Only .der and .pem certificates are supported".to_string()),
542        };
543
544        match cert {
545            Err(_) => Err(format!(
546                "Could not read cert from cert file {}",
547                path.display()
548            )),
549            Ok(val) => Ok(val),
550        }
551    }
552
553    /// Writes bytes to file and returns the size written, or an error reason for failure.
554    ///
555    /// # Errors
556    ///
557    /// A string description of any failure
558    ///
559    fn write_to_file(bytes: &[u8], file_path: &Path, overwrite: bool) -> Result<usize, String> {
560        if !overwrite && file_path.exists() {
561            Err(format!("File {} already exists and will not be overwritten. Enable overwrite to disable this safeguard.", file_path.display()))
562        } else {
563            if let Some(parent) = file_path.parent() {
564                CertificateStore::ensure_dir(parent)?;
565            }
566            match File::create(file_path) {
567                Ok(mut file) => file
568                    .write(bytes)
569                    .map_err(|_| format!("Could not write bytes to file {}", file_path.display())),
570                Err(_) => Err(format!("Could not create file {}", file_path.display())),
571            }
572        }
573    }
574}