opcua_crypto/
x509.rs

1// OPCUA for Rust
2// SPDX-License-Identifier: MPL-2.0
3// Copyright (C) 2017-2022 Adam Lock
4
5// X509 certificate wrapper.
6
7use std::{
8    self,
9    fmt::{self, Debug, Formatter},
10    net::{Ipv4Addr, Ipv6Addr},
11    result::Result,
12};
13
14use chrono::{DateTime, TimeZone, Utc};
15use openssl::{
16    asn1::*,
17    hash,
18    nid::Nid,
19    pkey,
20    rsa::*,
21    x509::{self, extension::*},
22};
23
24use opcua_types::{service_types::ApplicationDescription, status_code::StatusCode, ByteString};
25
26use crate::{
27    hostname,
28    pkey::{PrivateKey, PublicKey},
29    thumbprint::Thumbprint,
30};
31
32const DEFAULT_KEYSIZE: u32 = 2048;
33const DEFAULT_COUNTRY: &str = "IE";
34const DEFAULT_STATE: &str = "Dublin";
35
36#[derive(Debug)]
37/// Used to create an X509 cert (and private key)
38pub struct X509Data {
39    pub key_size: u32,
40    pub common_name: String,
41    pub organization: String,
42    pub organizational_unit: String,
43    pub country: String,
44    pub state: String,
45    /// A list of alternate host names as text. The first entry is expected to be the application uri.
46    /// The remainder are treated as IP addresses or DNS names depending on whether they parse as IPv4, IPv6 or neither.
47    /// IP addresses are expected to be in their canonical form and you will run into trouble
48    /// especially in IPv6 if they are not because string comparison may be used during validation.
49    /// e.g. IPv6 canonical format shortens addresses by stripping leading zeros, sequences of zeros
50    /// and using lowercase hex.
51    pub alt_host_names: Vec<String>,
52    /// The number of days the certificate is valid for, i.e. it will be valid from now until now + duration_days.
53    pub certificate_duration_days: u32,
54}
55
56impl From<(ApplicationDescription, Option<Vec<String>>)> for X509Data {
57    fn from(v: (ApplicationDescription, Option<Vec<String>>)) -> Self {
58        let (application_description, addresses) = v;
59        let application_uri = application_description.application_uri.as_ref();
60        let alt_host_names = Self::alt_host_names(application_uri, addresses, false, true);
61        X509Data {
62            key_size: DEFAULT_KEYSIZE,
63            common_name: application_description.application_name.to_string(),
64            organization: application_description.application_name.to_string(),
65            organizational_unit: application_description.application_name.to_string(),
66            country: DEFAULT_COUNTRY.to_string(),
67            state: DEFAULT_STATE.to_string(),
68            alt_host_names,
69            certificate_duration_days: 365,
70        }
71    }
72}
73
74impl From<ApplicationDescription> for X509Data {
75    fn from(v: ApplicationDescription) -> Self {
76        X509Data::from((v, None))
77    }
78}
79
80impl X509Data {
81    /// Gets a list of possible dns hostnames for this device
82    pub fn computer_hostnames() -> Vec<String> {
83        let mut result = Vec::with_capacity(2);
84
85        if let Ok(hostname) = hostname() {
86            if !hostname.is_empty() {
87                result.push(hostname);
88            }
89        }
90        if result.is_empty() {
91            // Look for environment vars
92            if let Ok(machine_name) = std::env::var("COMPUTERNAME") {
93                result.push(machine_name);
94            }
95            if let Ok(machine_name) = std::env::var("NAME") {
96                result.push(machine_name);
97            }
98        }
99
100        result
101    }
102
103    /// Creates a list of uri + DNS hostnames using the supplied arguments
104    pub fn alt_host_names(
105        application_uri: &str,
106        addresses: Option<Vec<String>>,
107        add_localhost: bool,
108        add_computer_name: bool,
109    ) -> Vec<String> {
110        // The first name is the application uri
111        let mut result = vec![application_uri.to_string()];
112
113        // Addresses supplied by caller
114        if let Some(mut addresses) = addresses {
115            result.append(&mut addresses);
116        }
117
118        // The remainder are alternative IP/DNS entries
119        if add_localhost {
120            result.push("localhost".to_string());
121            result.push("127.0.0.1".to_string());
122            result.push("::1".to_string());
123        }
124        // Get the machine name / ip address
125        if add_computer_name {
126            result.extend(Self::computer_hostnames());
127        }
128        if result.len() == 1 {
129            panic!("Could not create any DNS alt host names");
130        }
131        result
132    }
133
134    /// Creates a sample certificate for testing, sample purposes only
135    pub fn sample_cert() -> X509Data {
136        let alt_host_names = Self::alt_host_names("urn:OPCUADemo", None, false, true);
137        X509Data {
138            key_size: 2048,
139            common_name: "OPC UA Demo Key".to_string(),
140            organization: "OPC UA for Rust".to_string(),
141            organizational_unit: "OPC UA for Rust".to_string(),
142            country: DEFAULT_COUNTRY.to_string(),
143            state: DEFAULT_STATE.to_string(),
144            alt_host_names,
145            certificate_duration_days: 365,
146        }
147    }
148}
149
150#[derive(Debug)]
151pub struct X509Error;
152
153impl fmt::Display for X509Error {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        write!(f, "X509Error")
156    }
157}
158
159impl std::error::Error for X509Error {}
160
161/// This is a wrapper around the `OpenSSL` `X509` cert
162#[derive(Clone)]
163pub struct X509 {
164    value: x509::X509,
165}
166
167impl Debug for X509 {
168    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
169        // This impl will not write out the cert, and exists to keep derive happy
170        // on structs that contain an X509 instance
171        write!(f, "[x509]")
172    }
173}
174
175impl From<x509::X509> for X509 {
176    fn from(value: x509::X509) -> Self {
177        Self { value }
178    }
179}
180
181impl X509 {
182    pub fn from_der(der: &[u8]) -> Result<Self, X509Error> {
183        x509::X509::from_der(der).map(X509::from).map_err(|_| {
184            error!("Cannot produce an x509 cert from the data supplied");
185            X509Error
186        })
187    }
188
189    /// Creates a self-signed X509v3 certificate and public/private key from the supplied creation args.
190    /// The certificate identifies an instance of the application running on a host as well
191    /// as the public key. The PKey holds the corresponding public/private key. Note that if
192    /// the pkey is stored by cert store, then only the private key will be written. The public key
193    /// is only ever stored with the cert.
194    ///
195    /// See Part 6 Table 23 for full set of requirements
196    ///
197    /// In particular, application instance cert requires subjectAltName to specify alternate
198    /// hostnames / ip addresses that the host runs on.
199    pub fn cert_and_pkey(x509_data: &X509Data) -> Result<(Self, PrivateKey), String> {
200        // Create a key pair
201        let rsa = Rsa::generate(x509_data.key_size).map_err(|err| {
202            format!(
203                "Cannot create key pair check error {} and key size {}",
204                err, x509_data.key_size
205            )
206        })?;
207        let pkey = pkey::PKey::from_rsa(rsa)
208            .map_err(|err| format!("Cannot create key pair check error {}", err))?;
209        let pkey = PrivateKey::wrap_private_key(pkey);
210
211        // Create an X509 cert to hold the public key
212        let cert = Self::from_pkey(&pkey, x509_data)?;
213
214        Ok((cert, pkey))
215    }
216
217    pub fn from_pkey(pkey: &PrivateKey, x509_data: &X509Data) -> Result<Self, String> {
218        let mut builder = x509::X509Builder::new().unwrap();
219        // value 2 == version 3 (go figure)
220        let _ = builder.set_version(2);
221        let issuer_name = {
222            let mut name = x509::X509NameBuilder::new().unwrap();
223            // Common name
224            name.append_entry_by_text("CN", &x509_data.common_name)
225                .unwrap();
226            // Organization
227            name.append_entry_by_text("O", &x509_data.organization)
228                .unwrap();
229            // Organizational Unit
230            name.append_entry_by_text("OU", &x509_data.organizational_unit)
231                .unwrap();
232            // Country
233            name.append_entry_by_text("C", &x509_data.country).unwrap();
234            // State
235            name.append_entry_by_text("ST", &x509_data.state).unwrap();
236            name.build()
237        };
238        // Issuer and subject shall be the same for self-signed cert
239        let _ = builder.set_subject_name(&issuer_name);
240        let _ = builder.set_issuer_name(&issuer_name);
241
242        // For Application Instance Certificate specifies how cert may be used
243        let key_usage = KeyUsage::new()
244            .digital_signature()
245            .non_repudiation()
246            .key_encipherment()
247            .data_encipherment()
248            .key_cert_sign()
249            .build()
250            .unwrap();
251        let _ = builder.append_extension(key_usage);
252        let extended_key_usage = ExtendedKeyUsage::new()
253            .client_auth()
254            .server_auth()
255            .build()
256            .unwrap();
257        let _ = builder.append_extension(extended_key_usage);
258
259        builder
260            .set_not_before(&Asn1Time::days_from_now(0).unwrap())
261            .unwrap();
262        builder
263            .set_not_after(&Asn1Time::days_from_now(x509_data.certificate_duration_days).unwrap())
264            .unwrap();
265        builder.set_pubkey(&pkey.value).unwrap();
266
267        // Random serial number
268        {
269            use openssl::bn::BigNum;
270            use openssl::bn::MsbOption;
271            let mut serial = BigNum::new().unwrap();
272            serial.rand(128, MsbOption::MAYBE_ZERO, false).unwrap();
273            let serial = serial.to_asn1_integer().unwrap();
274            let _ = builder.set_serial_number(&serial);
275        }
276
277        // Subject alt names - The first is assumed to be the application uri. The remainder
278        // are either IP or DNS entries.
279        if !x509_data.alt_host_names.is_empty() {
280            let subject_alternative_name = {
281                let mut subject_alternative_name = SubjectAlternativeName::new();
282                x509_data
283                    .alt_host_names
284                    .iter()
285                    .enumerate()
286                    .for_each(|(i, alt_host_name)| {
287                        if !alt_host_name.is_empty() {
288                            if i == 0 {
289                                // The first entry is the application uri
290                                subject_alternative_name.uri(alt_host_name);
291                            } else if alt_host_name.parse::<Ipv4Addr>().is_ok()
292                                || alt_host_name.parse::<Ipv6Addr>().is_ok()
293                            {
294                                // Treat this as an IPv4/IPv6 address
295                                subject_alternative_name.ip(alt_host_name);
296                            } else {
297                                // Treat this as a DNS entry
298                                subject_alternative_name.dns(alt_host_name);
299                            }
300                        }
301                    });
302                subject_alternative_name
303                    .build(&builder.x509v3_context(None, None))
304                    .unwrap()
305            };
306            builder.append_extension(subject_alternative_name).unwrap();
307        }
308
309        // Self-sign
310        let _ = builder.sign(&pkey.value, hash::MessageDigest::sha256());
311
312        Ok(X509::from(builder.build()))
313    }
314
315    pub fn from_byte_string(data: &ByteString) -> Result<X509, StatusCode> {
316        if data.is_null() {
317            error!("Cannot make certificate from null bytestring");
318            Err(StatusCode::BadCertificateInvalid)
319        } else if let Ok(cert) = x509::X509::from_der(data.value.as_ref().unwrap()) {
320            Ok(X509::from(cert))
321        } else {
322            error!("Cannot make certificate, does bytestring contain .der?");
323            Err(StatusCode::BadCertificateInvalid)
324        }
325    }
326
327    /// Returns a ByteString representation of the cert which is DER encoded form of X509v3
328    pub fn as_byte_string(&self) -> ByteString {
329        let der = self.value.to_der().unwrap();
330        ByteString::from(&der)
331    }
332
333    pub fn public_key(&self) -> Result<PublicKey, StatusCode> {
334        self.value
335            .public_key()
336            .map(PublicKey::wrap_public_key)
337            .map_err(|_| {
338                error!("Cannot obtain public key from certificate");
339                StatusCode::BadCertificateInvalid
340            })
341    }
342
343    /// Returns the key length in bits (if possible)
344    pub fn key_length(&self) -> Result<usize, X509Error> {
345        let pub_key = self.value.public_key().map_err(|_| X509Error)?;
346        Ok(pub_key.size() * 8)
347    }
348
349    fn get_subject_entry(&self, nid: Nid) -> Result<String, X509Error> {
350        let subject_name = self.value.subject_name();
351        let mut entries = subject_name.entries_by_nid(nid);
352        if let Some(entry) = entries.next() {
353            // Asn1StringRef has to be converted out of Asn1 into UTF-8 and then a String
354            if let Ok(value) = entry.data().as_utf8() {
355                use std::ops::Deref;
356                // Value is an OpensslString type here so it has to be converted
357                Ok(value.deref().to_string())
358            } else {
359                Err(X509Error)
360            }
361        } else {
362            Err(X509Error)
363        }
364    }
365
366    // Produces a string such as "CN=foo/C=IE"
367    pub fn subject_name(&self) -> String {
368        use std::ops::Deref;
369        self.value
370            .subject_name()
371            .entries()
372            .map(|e| {
373                let v = if let Ok(v) = e.data().as_utf8() {
374                    v.deref().to_string()
375                } else {
376                    "?".into()
377                };
378                format!("{}={}", e.object(), v)
379            })
380            .collect::<Vec<String>>()
381            .join("/")
382    }
383
384    /// Gets the common name out of the cert
385    pub fn common_name(&self) -> Result<String, X509Error> {
386        self.get_subject_entry(Nid::COMMONNAME)
387    }
388
389    /// Tests if the certificate is valid for the supplied time using the not before and not
390    /// after values on the cert.
391    pub fn is_time_valid(&self, now: &DateTime<Utc>) -> StatusCode {
392        // Issuer time
393        let not_before = self.not_before();
394        if let Ok(not_before) = not_before {
395            if now.lt(&not_before) {
396                error!("Certificate < before date)");
397                return StatusCode::BadCertificateTimeInvalid;
398            }
399        } else {
400            // No before time
401            error!("Certificate has no before date");
402            return StatusCode::BadCertificateInvalid;
403        }
404
405        // Expiration time
406        let not_after = self.not_after();
407        if let Ok(not_after) = not_after {
408            if now.gt(&not_after) {
409                error!("Certificate has expired (> after date)");
410                return StatusCode::BadCertificateTimeInvalid;
411            }
412        } else {
413            // No after time
414            error!("Certificate has no after date");
415            return StatusCode::BadCertificateInvalid;
416        }
417
418        info!("Certificate is valid for this time");
419        StatusCode::Good
420    }
421
422    fn subject_alt_names(&self) -> Option<Vec<String>> {
423        if let Some(ref alt_names) = self.value.subject_alt_names() {
424            // Skip the application uri
425            let subject_alt_names = alt_names
426                .iter()
427                .skip(1)
428                .map(|n| {
429                    if let Some(dnsname) = n.dnsname() {
430                        dnsname.to_string()
431                    } else if let Some(ip) = n.ipaddress() {
432                        if ip.len() == 4 {
433                            let mut addr = [0u8; 4];
434                            addr[..].clone_from_slice(ip);
435                            Ipv4Addr::from(addr).to_string()
436                        } else if ip.len() == 16 {
437                            let mut addr = [0u8; 16];
438                            addr[..].clone_from_slice(ip);
439                            Ipv6Addr::from(addr).to_string()
440                        } else {
441                            "".to_string()
442                        }
443                    } else {
444                        "".to_string()
445                    }
446                })
447                .collect();
448            Some(subject_alt_names)
449        } else {
450            None
451        }
452    }
453
454    /// Tests if the supplied hostname matches any of the dns alt subject name entries on the cert
455    pub fn is_hostname_valid(&self, hostname: &str) -> StatusCode {
456        trace!("is_hostname_valid against {} on cert", hostname);
457        // Look through alt subject names for a matching entry
458        if hostname.is_empty() {
459            error!("Hostname is empty");
460            StatusCode::BadCertificateHostNameInvalid
461        } else if let Some(subject_alt_names) = self.subject_alt_names() {
462            let found = subject_alt_names
463                .iter()
464                .any(|n| n.eq_ignore_ascii_case(hostname));
465            if found {
466                info!("Certificate host name {} is good", hostname);
467                StatusCode::Good
468            } else {
469                let alt_names = subject_alt_names
470                    .iter()
471                    .map(|n| n.as_ref())
472                    .collect::<Vec<&str>>()
473                    .join(", ");
474                error!(
475                    "Cannot find a matching hostname for input {}, alt names = {}",
476                    hostname, alt_names
477                );
478                StatusCode::BadCertificateHostNameInvalid
479            }
480        } else {
481            // No alt names
482            error!("Cert has no subject alt names at all");
483            StatusCode::BadCertificateHostNameInvalid
484        }
485    }
486
487    /// Tests if the supplied application uri matches the uri alt subject name entry on the cert
488    pub fn is_application_uri_valid(&self, application_uri: &str) -> StatusCode {
489        trace!(
490            "is_application_uri_valid against {} on cert",
491            application_uri
492        );
493        // Expecting the first subject alternative name to be a uri that matches with the supplied
494        // application uri
495        if let Some(ref alt_names) = self.value.subject_alt_names() {
496            if alt_names.len() > 0 {
497                if let Some(cert_application_uri) = alt_names[0].uri() {
498                    if cert_application_uri == application_uri {
499                        info!("Certificate application uri {} is good", application_uri);
500                        StatusCode::Good
501                    } else {
502                        error!(
503                            "Cert application uri {} does not match supplied uri {}",
504                            cert_application_uri, application_uri
505                        );
506                        StatusCode::BadCertificateUriInvalid
507                    }
508                } else {
509                    error!("Cert's first subject alt name is not a uri and cannot be compared");
510                    StatusCode::BadCertificateUriInvalid
511                }
512            } else {
513                error!("Cert has zero subject alt names");
514                StatusCode::BadCertificateUriInvalid
515            }
516        } else {
517            error!("Cert has no subject alt names at all");
518            // No alt names
519            StatusCode::BadCertificateUriInvalid
520        }
521    }
522
523    /// OPC UA Part 6 MessageChunk structure
524    ///
525    /// The thumbprint is the SHA1 digest of the DER form of the certificate. The hash is 160 bits
526    /// (20 bytes) in length and is sent in some secure conversation headers.
527    ///
528    /// The thumbprint might be used by the server / client for look-up purposes.
529    pub fn thumbprint(&self) -> Thumbprint {
530        use openssl::hash::{hash, MessageDigest};
531        let der = self.value.to_der().unwrap();
532        let digest = hash(MessageDigest::sha1(), &der).unwrap();
533        Thumbprint::new(&digest)
534    }
535
536    /// Turn the Asn1 values into useful portable types
537    pub fn not_before(&self) -> Result<DateTime<Utc>, X509Error> {
538        let date = self.value.not_before().to_string();
539        Self::parse_asn1_date(&date)
540    }
541
542    /// Turn the Asn1 values into useful portable types
543    pub fn not_after(&self) -> Result<DateTime<Utc>, X509Error> {
544        let date = self.value.not_after().to_string();
545        Self::parse_asn1_date(&date)
546    }
547
548    pub fn to_der(&self) -> Result<Vec<u8>, X509Error> {
549        self.value.to_der().map_err(|e| {
550            error!("Cannot turn X509 cert to DER, err = {:?}", e);
551            X509Error
552        })
553    }
554
555    fn parse_asn1_date(date: &str) -> Result<DateTime<Utc>, X509Error> {
556        const SUFFIX: &str = " GMT";
557        // Parse ASN1 time format
558        // MMM DD HH:MM:SS YYYY [GMT]
559        let date = if date.ends_with(SUFFIX) {
560            // Not interested in GMT part, ASN1 is always GMT (i.e. UTC)
561            let end = date.len() - SUFFIX.len();
562            &date[..end]
563        } else {
564            date
565        };
566        Utc.datetime_from_str(date, "%b %d %H:%M:%S %Y")
567            .map_err(|e| {
568                error!("Cannot parse ASN1 date, err = {:?}", e);
569                X509Error
570            })
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn parse_asn1_date_test() {
580        use chrono::{Datelike, Timelike};
581
582        assert!(X509::parse_asn1_date("").is_err());
583        assert!(X509::parse_asn1_date("Jan 69 00:00:00 1970").is_err());
584        assert!(X509::parse_asn1_date("Feb 21 00:00:00 1970").is_ok());
585        assert!(X509::parse_asn1_date("Feb 21 00:00:00 1970 GMT").is_ok());
586
587        let dt: DateTime<Utc> = X509::parse_asn1_date("Feb 21 12:45:30 1999 GMT").unwrap();
588        assert_eq!(dt.month(), 2);
589        assert_eq!(dt.day(), 21);
590        assert_eq!(dt.hour(), 12);
591        assert_eq!(dt.minute(), 45);
592        assert_eq!(dt.second(), 30);
593        assert_eq!(dt.year(), 1999);
594    }
595
596    /// This test checks that a cert will validate dns or ip entries in the subject alt host names
597    #[test]
598    fn alt_hostnames() {
599        opcua_console_logging::init();
600
601        let alt_host_names = ["uri:foo", "host2", "www.google.com", "192.168.1.1", "::1"];
602
603        // Create a cert with alt hostnames which are both IP and DNS entries
604        let args = X509Data {
605            key_size: 2048,
606            common_name: "x".to_string(),
607            organization: "x.org".to_string(),
608            organizational_unit: "x.org ops".to_string(),
609            country: "EN".to_string(),
610            state: "London".to_string(),
611            alt_host_names: alt_host_names.iter().map(|h| h.to_string()).collect(),
612            certificate_duration_days: 60,
613        };
614
615        let (x509, _pkey) = X509::cert_and_pkey(&args).unwrap();
616
617        assert!(!x509.is_hostname_valid("").is_good());
618        assert!(!x509.is_hostname_valid("uri:foo").is_good()); // The application uri should not be valid
619        assert!(!x509.is_hostname_valid("192.168.1.0").is_good());
620        assert!(!x509.is_hostname_valid("www.cnn.com").is_good());
621        assert!(!x509.is_hostname_valid("host1").is_good());
622
623        alt_host_names.iter().skip(1).for_each(|n| {
624            println!("Hostname {}", n);
625            assert!(x509.is_hostname_valid(n).is_good());
626        })
627    }
628}