use std::{
self,
fmt::{Debug, Formatter},
net::{Ipv4Addr, Ipv6Addr},
result::Result,
};
use chrono::{DateTime, TimeZone, Utc};
use openssl::{
asn1::*,
hash,
nid::Nid,
pkey,
rsa::*,
x509::{self, extension::*},
};
use opcua_types::{ByteString, service_types::ApplicationDescription, status_code::StatusCode};
use crate::{
hostname,
pkey::{PrivateKey, PublicKey},
thumbprint::Thumbprint,
};
const DEFAULT_KEYSIZE: u32 = 2048;
const DEFAULT_COUNTRY: &str = "IE";
const DEFAULT_STATE: &str = "Dublin";
#[derive(Debug)]
pub struct X509Data {
pub key_size: u32,
pub common_name: String,
pub organization: String,
pub organizational_unit: String,
pub country: String,
pub state: String,
pub alt_host_names: Vec<String>,
pub certificate_duration_days: u32,
}
impl From<(ApplicationDescription, Option<Vec<String>>)> for X509Data {
fn from(v: (ApplicationDescription, Option<Vec<String>>)) -> Self {
let (application_description, addresses) = v;
let application_uri = application_description.application_uri.as_ref();
let alt_host_names = Self::alt_host_names(application_uri, addresses, false, true);
X509Data {
key_size: DEFAULT_KEYSIZE,
common_name: application_description.application_name.to_string(),
organization: application_description.application_name.to_string(),
organizational_unit: application_description.application_name.to_string(),
country: DEFAULT_COUNTRY.to_string(),
state: DEFAULT_STATE.to_string(),
alt_host_names,
certificate_duration_days: 365,
}
}
}
impl From<ApplicationDescription> for X509Data {
fn from(v: ApplicationDescription) -> Self {
X509Data::from((v, None))
}
}
impl X509Data {
pub fn computer_hostnames() -> Vec<String> {
let mut result = Vec::with_capacity(2);
if let Ok(hostname) = hostname() {
if !hostname.is_empty() {
result.push(hostname);
}
}
if result.is_empty() {
if let Ok(machine_name) = std::env::var("COMPUTERNAME") {
result.push(machine_name);
}
if let Ok(machine_name) = std::env::var("NAME") {
result.push(machine_name);
}
}
result
}
pub fn alt_host_names(application_uri: &str, addresses: Option<Vec<String>>, add_localhost: bool, add_computer_name: bool) -> Vec<String> {
let mut result = vec![application_uri.to_string()];
if let Some(mut addresses) = addresses {
result.append(&mut addresses);
}
if add_localhost {
result.push("localhost".to_string());
result.push("127.0.0.1".to_string());
result.push("::1".to_string());
}
if add_computer_name {
result.extend(Self::computer_hostnames());
}
if result.len() == 1 {
panic!("Could not create any DNS alt host names");
}
result
}
pub fn sample_cert() -> X509Data {
let alt_host_names = Self::alt_host_names("urn:OPCUADemo", None, false, true);
X509Data {
key_size: 2048,
common_name: "OPC UA Demo Key".to_string(),
organization: "OPC UA for Rust".to_string(),
organizational_unit: "OPC UA for Rust".to_string(),
country: DEFAULT_COUNTRY.to_string(),
state: DEFAULT_STATE.to_string(),
alt_host_names,
certificate_duration_days: 365,
}
}
}
#[derive(Clone)]
pub struct X509 {
value: x509::X509,
}
impl Debug for X509 {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "[x509]")
}
}
impl From<x509::X509> for X509 {
fn from(value: x509::X509) -> Self {
Self { value }
}
}
impl X509 {
pub fn from_der(der: &[u8]) -> Result<Self, ()> {
x509::X509::from_der(der)
.map(|value| X509::from(value))
.map_err(|_| {
error!("Cannot produce an x509 cert from the data supplied");
})
}
pub fn cert_and_pkey(x509_data: &X509Data) -> Result<(Self, PrivateKey), String> {
let rsa = Rsa::generate(x509_data.key_size)
.map_err(|err| {
format!("Cannot create key pair check error {} and key size {}", err.to_string(), x509_data.key_size)
})?;
let pkey = pkey::PKey::from_rsa(rsa)
.map_err(|err| {
format!("Cannot create key pair check error {}", err.to_string())
})?;
let pkey = PrivateKey::wrap_private_key(pkey);
let cert = Self::from_pkey(&pkey, x509_data)?;
Ok((cert, pkey))
}
pub fn from_pkey(pkey: &PrivateKey, x509_data: &X509Data) -> Result<Self, String> {
let mut builder = x509::X509Builder::new().unwrap();
let _ = builder.set_version(2);
let issuer_name = {
let mut name = x509::X509NameBuilder::new().unwrap();
name.append_entry_by_text("CN", &x509_data.common_name).unwrap();
name.append_entry_by_text("O", &x509_data.organization).unwrap();
name.append_entry_by_text("OU", &x509_data.organizational_unit).unwrap();
name.append_entry_by_text("C", &x509_data.country).unwrap();
name.append_entry_by_text("ST", &x509_data.state).unwrap();
name.build()
};
let _ = builder.set_subject_name(&issuer_name);
let _ = builder.set_issuer_name(&issuer_name);
let key_usage = KeyUsage::new().
digital_signature().
non_repudiation().
key_encipherment().
data_encipherment().
key_cert_sign().
build().unwrap();
let _ = builder.append_extension(key_usage);
let extended_key_usage = ExtendedKeyUsage::new().
client_auth().
server_auth().build().unwrap();
let _ = builder.append_extension(extended_key_usage);
builder.set_not_before(&Asn1Time::days_from_now(0).unwrap()).unwrap();
builder.set_not_after(&Asn1Time::days_from_now(x509_data.certificate_duration_days).unwrap()).unwrap();
builder.set_pubkey(&pkey.value).unwrap();
{
use openssl::bn::BigNum;
use openssl::bn::MsbOption;
let mut serial = BigNum::new().unwrap();
serial.rand(128, MsbOption::MAYBE_ZERO, false).unwrap();
let serial = serial.to_asn1_integer().unwrap();
let _ = builder.set_serial_number(&serial);
}
if !x509_data.alt_host_names.is_empty() {
let subject_alternative_name = {
let mut subject_alternative_name = SubjectAlternativeName::new();
x509_data.alt_host_names.iter().enumerate().for_each(|(i, alt_host_name)| {
if !alt_host_name.is_empty() {
if i == 0 {
subject_alternative_name.uri(alt_host_name);
} else if let Ok(_) = alt_host_name.parse::<Ipv4Addr>() {
subject_alternative_name.ip(alt_host_name);
} else if let Ok(_) = alt_host_name.parse::<Ipv6Addr>() {
subject_alternative_name.ip(alt_host_name);
} else {
subject_alternative_name.dns(alt_host_name);
}
}
});
subject_alternative_name.build(&builder.x509v3_context(None, None)).unwrap()
};
builder.append_extension(subject_alternative_name).unwrap();
}
let _ = builder.sign(&pkey.value, hash::MessageDigest::sha256());
Ok(X509::from(builder.build()))
}
pub fn from_byte_string(data: &ByteString) -> Result<X509, StatusCode> {
if data.is_null() {
error!("Cannot make certificate from null bytestring");
Err(StatusCode::BadCertificateInvalid)
} else if let Ok(cert) = x509::X509::from_der(&data.value.as_ref().unwrap()) {
Ok(X509::from(cert))
} else {
error!("Cannot make certificate, does bytestring contain .der?");
Err(StatusCode::BadCertificateInvalid)
}
}
pub fn as_byte_string(&self) -> ByteString {
let der = self.value.to_der().unwrap();
ByteString::from(&der)
}
pub fn public_key(&self) -> Result<PublicKey, StatusCode> {
self.value.public_key()
.map(|pkey| PublicKey::wrap_public_key(pkey))
.map_err(|_| {
error!("Cannot obtain public key from certificate");
StatusCode::BadCertificateInvalid
})
}
pub fn key_length(&self) -> Result<usize, ()> {
let pub_key = self.value.public_key().map_err(|_| ())?;
Ok(pub_key.size() * 8)
}
fn get_subject_entry(&self, nid: Nid) -> Result<String, ()> {
let subject_name = self.value.subject_name();
let mut entries = subject_name.entries_by_nid(nid);
if let Some(entry) = entries.next() {
if let Ok(value) = entry.data().as_utf8() {
use std::ops::Deref;
Ok(value.deref().to_string())
} else {
Err(())
}
} else {
Err(())
}
}
pub fn subject_name(&self) -> String {
use std::ops::Deref;
self.value.subject_name().entries()
.map(|e| {
let v = if let Ok(v) = e.data().as_utf8() {
v.deref().to_string()
} else {
"?".into()
};
format!("{}={}", e.object(), v)
})
.collect::<Vec<String>>()
.join("/")
}
pub fn common_name(&self) -> Result<String, ()> {
self.get_subject_entry(Nid::COMMONNAME)
}
pub fn is_time_valid(&self, now: &DateTime<Utc>) -> StatusCode {
let not_before = self.not_before();
if let Ok(not_before) = not_before {
if now.lt(¬_before) {
error!("Certificate < before date)");
return StatusCode::BadCertificateTimeInvalid;
}
} else {
error!("Certificate has no before date");
return StatusCode::BadCertificateInvalid;
}
let not_after = self.not_after();
if let Ok(not_after) = not_after {
if now.gt(¬_after) {
error!("Certificate has expired (> after date)");
return StatusCode::BadCertificateTimeInvalid;
}
} else {
error!("Certificate has no after date");
return StatusCode::BadCertificateInvalid;
}
info!("Certificate is valid for this time");
StatusCode::Good
}
fn subject_alt_names(&self) -> Option<Vec<String>> {
if let Some(ref alt_names) = self.value.subject_alt_names() {
let subject_alt_names = alt_names.iter().skip(1).map(|n| {
if let Some(dnsname) = n.dnsname() {
dnsname.to_string()
} else if let Some(ip) = n.ipaddress() {
if ip.len() == 4 {
let mut addr = [0u8; 4];
addr[..].clone_from_slice(&ip);
Ipv4Addr::from(addr).to_string()
} else if ip.len() == 16 {
let mut addr = [0u8; 16];
addr[..].clone_from_slice(&ip);
Ipv6Addr::from(addr).to_string()
} else {
"".to_string()
}
} else {
"".to_string()
}
}).collect();
Some(subject_alt_names)
} else {
None
}
}
pub fn is_hostname_valid(&self, hostname: &str) -> StatusCode {
trace!("is_hostname_valid against {} on cert", hostname);
if hostname.is_empty() {
error!("Hostname is empty");
StatusCode::BadCertificateHostNameInvalid
} else if let Some(subject_alt_names) = self.subject_alt_names() {
let found = subject_alt_names.iter().any(|n| {
n.eq_ignore_ascii_case(hostname)
});
if found {
info!("Certificate host name {} is good", hostname);
StatusCode::Good
} else {
let alt_names = subject_alt_names.iter().map(|n| n.as_ref()).collect::<Vec<&str>>().join(", ");
error!("Cannot find a matching hostname for input {}, alt names = {}", hostname, alt_names);
StatusCode::BadCertificateHostNameInvalid
}
} else {
error!("Cert has no subject alt names at all");
StatusCode::BadCertificateHostNameInvalid
}
}
pub fn is_application_uri_valid(&self, application_uri: &str) -> StatusCode {
trace!("is_application_uri_valid against {} on cert", application_uri);
if let Some(ref alt_names) = self.value.subject_alt_names() {
if alt_names.len() > 0 {
if let Some(cert_application_uri) = alt_names[0].uri() {
if cert_application_uri == application_uri {
info!("Certificate application uri {} is good", application_uri);
StatusCode::Good
} else {
error!("Cert application uri {} does not match supplied uri {}", cert_application_uri, application_uri);
StatusCode::BadCertificateUriInvalid
}
} else {
error!("Cert's first subject alt name is not a uri and cannot be compared");
StatusCode::BadCertificateUriInvalid
}
} else {
error!("Cert has zero subject alt names");
StatusCode::BadCertificateUriInvalid
}
} else {
error!("Cert has no subject alt names at all");
StatusCode::BadCertificateUriInvalid
}
}
pub fn thumbprint(&self) -> Thumbprint {
use openssl::hash::{MessageDigest, hash};
let der = self.value.to_der().unwrap();
let digest = hash(MessageDigest::sha1(), &der).unwrap();
Thumbprint::new(&digest)
}
pub fn not_before(&self) -> Result<DateTime<Utc>, ()> {
let date = self.value.not_before().to_string();
Self::parse_asn1_date(&date)
}
pub fn not_after(&self) -> Result<DateTime<Utc>, ()> {
let date = self.value.not_after().to_string();
Self::parse_asn1_date(&date)
}
pub fn to_der(&self) -> Result<Vec<u8>, ()> {
self.value.to_der().map_err(|e| {
error!("Cannot turn X509 cert to DER, err = {:?}", e);
})
}
fn parse_asn1_date(date: &str) -> Result<DateTime<Utc>, ()> {
let date = if date.ends_with(" GMT") {
&date[..date.len() - 4]
} else {
&date
};
Utc.datetime_from_str(date, "%b %d %H:%M:%S %Y").map_err(|e| {
error!("Cannot parse ASN1 date, err = {:?}", e);
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_asn1_date_test() {
use chrono::{Datelike, Timelike};
assert!(X509::parse_asn1_date("").is_err());
assert!(X509::parse_asn1_date("Jan 69 00:00:00 1970").is_err());
assert!(X509::parse_asn1_date("Feb 21 00:00:00 1970").is_ok());
assert!(X509::parse_asn1_date("Feb 21 00:00:00 1970 GMT").is_ok());
let dt: DateTime<Utc> = X509::parse_asn1_date("Feb 21 12:45:30 1999 GMT").unwrap();
assert_eq!(dt.month(), 2);
assert_eq!(dt.day(), 21);
assert_eq!(dt.hour(), 12);
assert_eq!(dt.minute(), 45);
assert_eq!(dt.second(), 30);
assert_eq!(dt.year(), 1999);
}
#[test]
fn alt_hostnames() {
opcua_console_logging::init();
let alt_host_names = ["uri:foo", "host2", "www.google.com", "192.168.1.1", "::1"];
let args = X509Data {
key_size: 2048,
common_name: "x".to_string(),
organization: "x.org".to_string(),
organizational_unit: "x.org ops".to_string(),
country: "EN".to_string(),
state: "London".to_string(),
alt_host_names: alt_host_names.iter().map(|h| h.to_string()).collect(),
certificate_duration_days: 60,
};
let (x509, _pkey) = X509::cert_and_pkey(&args).unwrap();
assert!(!x509.is_hostname_valid("").is_good());
assert!(!x509.is_hostname_valid("uri:foo").is_good());
assert!(!x509.is_hostname_valid("192.168.1.0").is_good());
assert!(!x509.is_hostname_valid("www.cnn.com").is_good());
assert!(!x509.is_hostname_valid("host1").is_good());
alt_host_names.iter().skip(1).for_each(|n| {
println!("Hostname {}", n);
assert!(x509.is_hostname_valid(n).is_good());
})
}
}