diff --git a/CHANGELOG.md b/CHANGELOG.md index d4677ae..2afa37f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added - Add type `PrivateKey` to wrap private keys when using Mbed TLS. +- Add types `ua::SecurityLevel`, `ua::EndpointDescription`, `ua::MessageSecurityMode`. +- Add method `ClientBuilder::get_endpoints()` to get remote server endpoints. +- Add method `ClientBuilder::certificate_verification()` and type `ua::CertificateVerification`. +- Add method `ua::CertificateVerification::custom()` and trait `CustomCertificateVerification` to + allow custom certificate verification. ### Changed - Breaking: Bump Minimum Supported Rust Version (MSRV) to 1.80. - Breaking: Change type `Certificate` to hold certificate without private key. - Breaking: Use new types `Certificate` and `PrivateKey` instead of raw `&[u8]` in - `ua::ClientBuilder::default_encryption()`, `ua::ServerBuilder::default_with_security_policies()`. + `ClientBuilder::default_encryption()`, `ServerBuilder::default_with_security_policies()`. ## [0.6.6] - 2024-12-04 diff --git a/Cargo.lock b/Cargo.lock index 3a71f47..c5e6b5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "annotate-snippets" version = "0.9.2" @@ -111,6 +126,28 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcder" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627747a6774aab38beb35990d88309481378558875a41da1a4b2e373c906ef0" +dependencies = [ + "bytes", + "smallvec", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -121,7 +158,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "log", "prettyplease", "proc-macro2", @@ -138,6 +175,18 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + [[package]] name = "cc" version = "1.0.83" @@ -162,6 +211,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.0", +] + [[package]] name = "clang-sys" version = "1.7.0" @@ -188,6 +249,28 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -345,12 +428,41 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "itertools" version = "0.12.1" @@ -360,12 +472,31 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.153" @@ -425,6 +556,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -444,6 +584,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "open62541" version = "0.6.6" @@ -454,15 +600,17 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "itertools 0.13.0", "log", "open62541-sys", "paste", "rand", "serde", "serde_json", - "thiserror", + "thiserror 2.0.3", "time", "tokio", + "x509-certificate", ] [[package]] @@ -483,6 +631,16 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -594,6 +752,21 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -649,6 +822,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slab" version = "0.4.9" @@ -658,6 +840,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "syn" version = "2.0.87" @@ -669,13 +873,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -755,6 +979,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf8parse" version = "0.2.1" @@ -773,6 +1003,61 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" + [[package]] name = "winapi" version = "0.3.9" @@ -795,6 +1080,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -927,6 +1221,25 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "x509-certificate" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57b9f8bcae7c1f36479821ae826d75050c60ce55146fd86d3553ed2573e2762" +dependencies = [ + "bcder", + "bytes", + "chrono", + "der", + "hex", + "pem", + "ring", + "signature", + "spki", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "yansi-term" version = "0.1.2" @@ -935,3 +1248,23 @@ checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" dependencies = [ "winapi", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 4a53696..353300a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,11 +32,13 @@ tokio = { version = "1.35.1", optional = true, features = [ "sync", "time", ] } +x509-certificate = { version = "0.24.0", optional = true } [dev-dependencies] anyhow = "1.0.79" -futures = "0.3.30" env_logger = "0.11.1" +futures = "0.3.30" +itertools = "0.13.0" rand = "0.8.5" time = { version = "0.3.31", features = ["macros"] } # Enable multi-threaded runtime in examples to increase the chances of finding @@ -49,6 +51,7 @@ mbedtls = ["open62541-sys/mbedtls"] serde = ["dep:serde", "dep:serde_json", "time?/formatting", "time?/serde"] time = ["dep:time"] tokio = ["dep:tokio"] +x509 = ["dep:x509-certificate"] [lints.rust] future_incompatible = { level = "warn", priority = -1 } @@ -185,4 +188,8 @@ name = "server_method_callback" [[example]] name = "ssl_create_certificate" -required-features = ["mbedtls"] +required-features = ["mbedtls", "x509"] + +[[example]] +name = "ssl_fetch_certificate" +required-features = ["mbedtls", "x509"] diff --git a/examples/ssl_create_certificate.rs b/examples/ssl_create_certificate.rs index 1129a9a..3e239ae 100644 --- a/examples/ssl_create_certificate.rs +++ b/examples/ssl_create_certificate.rs @@ -1,5 +1,3 @@ -use std::io::{self, Write}; - use anyhow::Context as _; use open62541::ua; @@ -31,15 +29,44 @@ fn main() -> anyhow::Result<()> { let (certificate, _private_key) = open62541::create_certificate( &subject, &subject_alt_name, - &ua::CertificateFormat::PEM, + &ua::CertificateFormat::DER, Some(¶ms), ) .context("create certificate")?; - let certificate_pem = certificate.as_bytes(); - io::stdout() - .write_all(certificate_pem) - .context("write certificate")?; + let certificate = certificate.into_x509().context("parse certificate")?; + + println!( + "Subject common name: {:?}", + certificate.subject_common_name() + ); + println!("Key algorithm: {:?}", certificate.key_algorithm()); + println!( + "Signature algorithm: {:?}", + certificate.signature_algorithm() + ); + println!( + "Validity not before: {:?}", + certificate.validity_not_before() + ); + println!("Validity not after: {:?}", certificate.validity_not_after()); + println!( + "Fingerprint (SHA-1): {:?}", + certificate + .sha1_fingerprint() + .context("SHA-1 fingerprint")? + ); + println!( + "Fingerprint (SHA-256): {:?}", + certificate + .sha256_fingerprint() + .context("SHA-256 fingerprint")? + ); + println!(); + println!( + "{}", + certificate.encode_pem().context("encode certificate")? + ); Ok(()) } diff --git a/examples/ssl_fetch_certificate.rs b/examples/ssl_fetch_certificate.rs new file mode 100644 index 0000000..c95471e --- /dev/null +++ b/examples/ssl_fetch_certificate.rs @@ -0,0 +1,68 @@ +use anyhow::Context as _; +use itertools::Itertools as _; +use open62541::{Certificate, ClientBuilder}; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let endpoint_descriptions = ClientBuilder::default() + .get_endpoints("opc.tcp://localhost") + .context("get endpoints")?; + + let server_certificates = endpoint_descriptions + .iter() + .filter_map(|endpoint_description| { + endpoint_description + .server_certificate() + .as_bytes() + .map(|bytes| Certificate::from_bytes(bytes).into_x509()) + }) + .collect::, _>>() + .context("parse certificates")?; + + // Include consecutive (!) identical certificates only once. + let unique_certificates = server_certificates + .into_iter() + .dedup_by(|a, b| a.serial_number_asn1() == b.serial_number_asn1()) + .collect::>(); + + println!("Found {} server certificate(s)", unique_certificates.len()); + + for (index, certificate) in unique_certificates.iter().enumerate() { + println!(); + println!("# Certificate {}", index + 1); + println!( + "Subject common name: {:?}", + certificate.subject_common_name() + ); + println!("Key algorithm: {:?}", certificate.key_algorithm()); + println!( + "Signature algorithm: {:?}", + certificate.signature_algorithm() + ); + println!( + "Validity not before: {:?}", + certificate.validity_not_before() + ); + println!("Validity not after: {:?}", certificate.validity_not_after()); + println!( + "Fingerprint (SHA-1): {:?}", + certificate + .sha1_fingerprint() + .context("SHA-1 fingerprint")? + ); + println!( + "Fingerprint (SHA-256): {:?}", + certificate + .sha256_fingerprint() + .context("SHA-256 fingerprint")? + ); + println!(); + println!( + "{}", + certificate.encode_pem().context("encode certificate")? + ); + } + + Ok(()) +} diff --git a/src/client.rs b/src/client.rs index b573ee5..daf9cea 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,6 +1,9 @@ -use std::{ffi::CString, time::Duration}; +use std::{ffi::CString, ptr, time::Duration}; -use open62541_sys::{UA_CertificateVerification_AcceptAll, UA_ClientConfig, UA_Client_connect}; +use open62541_sys::{ + UA_CertificateVerification_AcceptAll, UA_ClientConfig, UA_Client_connect, + UA_Client_getEndpoints, +}; use crate::{ua, DataType as _, Error, Result}; @@ -161,6 +164,9 @@ impl ClientBuilder { /// /// Note that this disables all certificate verification of server communications. Use only when /// servers can be identified in some other way, or identity is not relevant. + /// + /// This is a shortcut for using [`certificate_verification()`](Self::certificate_verification) + /// with [`ua::CertificateVerification::accept_all()`]. #[must_use] pub fn accept_all(mut self) -> Self { let config = self.config_mut(); @@ -170,6 +176,17 @@ impl ClientBuilder { self } + /// Sets certificate verification. + #[must_use] + pub fn certificate_verification( + mut self, + certificate_verification: ua::CertificateVerification, + ) -> Self { + let config = self.config_mut(); + certificate_verification.move_into_raw(&mut config.certificateVerification); + self + } + /// Connects to OPC UA endpoint and returns [`Client`]. /// /// # Errors @@ -185,6 +202,51 @@ impl ClientBuilder { Ok(client) } + /// Connects to OPC UA server and returns endpoints. + /// + /// # Errors + /// + /// This fails when the target server is not reachable. + /// + /// # Panics + /// + /// The server URL must not contain any NUL bytes. + pub fn get_endpoints(self, server_url: &str) -> Result> { + log::info!("Getting endpoints of server {server_url}"); + + let server_url = CString::new(server_url).expect("server URL does not contain NUL bytes"); + + let mut client = self.build(); + let endpoint_descriptions: Option>; + + let status_code = ua::StatusCode::new({ + let mut endpoint_descriptions_size = 0; + let mut endpoint_descriptions_ptr = ptr::null_mut(); + let result = unsafe { + UA_Client_getEndpoints( + client.0.as_mut_ptr(), + server_url.as_ptr(), + &mut endpoint_descriptions_size, + &mut endpoint_descriptions_ptr, + ) + }; + // Wrap array result immediately to not leak memory when leaving function early as with + // `?` below. + endpoint_descriptions = ua::Array::::from_raw_parts( + endpoint_descriptions_size, + endpoint_descriptions_ptr, + ); + result + }); + Error::verify_good(&status_code)?; + + let Some(endpoint_descriptions) = endpoint_descriptions else { + return Err(Error::internal("expected array of endpoint descriptions")); + }; + + Ok(endpoint_descriptions) + } + /// Builds OPC UA client. #[must_use] fn build(self) -> Client { diff --git a/src/data_type.rs b/src/data_type.rs index aca87a6..db5d2ab 100644 --- a/src/data_type.rs +++ b/src/data_type.rs @@ -409,12 +409,12 @@ macro_rules! data_type { #[must_use] fn into_raw(self) -> Self::Inner { - // SAFETY: Move value out of `self` despite it not being `Copy`. We consume `self` - // and forget it below, so that `Drop` is not called on the original value. - let inner = unsafe { std::ptr::read(std::ptr::addr_of!(self.0)) }; - // Make sure that `drop()` is not called anymore. - std::mem::forget(self); - inner + // Use `ManuallyDrop` to avoid double-free even when added code might cause panic. + // See documentation of `mem::forget()` for details. + let this = std::mem::ManuallyDrop::new(self); + // SAFETY: Aliasing memory temporarily is safe because destructor will not be + // called. + unsafe { std::ptr::read(std::ptr::addr_of!(this.0)) } } } diff --git a/src/lib.rs b/src/lib.rs index b48c76a..58cc018 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -208,13 +208,6 @@ //! # } //! ``` -mod client; -mod data_type; -mod error; -mod server; -mod service; -pub mod ua; - #[cfg(feature = "tokio")] mod async_client; #[cfg(feature = "tokio")] @@ -225,10 +218,16 @@ mod attributes; mod browse_result; #[cfg(feature = "tokio")] mod callback; +mod client; +mod data_type; mod data_value; +mod error; +mod server; +mod service; #[cfg(feature = "mbedtls")] mod ssl; mod traits; +pub mod ua; mod userdata; mod value; @@ -253,7 +252,7 @@ pub use self::{ MethodCallback, MethodCallbackContext, MethodCallbackError, MethodCallbackResult, MethodNode, Node, ObjectNode, Server, ServerBuilder, ServerRunner, VariableNode, }, - traits::{Attribute, Attributes}, + traits::{Attribute, Attributes, CustomCertificateVerification}, userdata::{Userdata, UserdataSentinel}, value::{ScalarValue, ValueType, VariantValue}, }; diff --git a/src/ssl.rs b/src/ssl.rs index 0868977..16b36ea 100644 --- a/src/ssl.rs +++ b/src/ssl.rs @@ -2,7 +2,7 @@ use std::{fmt, ptr}; use open62541_sys::UA_CreateCertificate; -use crate::{ua, DataType, Error, Result}; +use crate::{ua, DataType, Error}; /// Certificate in [DER] or [PEM] format. /// @@ -36,6 +36,26 @@ impl Certificate { unsafe { self.0.as_bytes_unchecked() } } + /// Parses certificate. + /// + /// # Errors + /// + /// This fails when the certificate cannot be parsed or is invalid. + #[cfg(feature = "x509")] + pub fn into_x509( + self, + ) -> Result { + use x509_certificate::{X509Certificate, X509CertificateError}; + + // Apply heuristic to get certificates from both DER and PEM format. Try PEM first because + // the implementation first extracts DER data from PEM and can tell us whether this failed + // (or the certificate itself was invalid). + X509Certificate::from_pem(self.as_bytes()).or_else(|err| match err { + X509CertificateError::PemDecode(_) => X509Certificate::from_der(self.as_bytes()), + err => Err(err), + }) + } + pub(crate) const fn as_byte_string(&self) -> &ua::ByteString { &self.0 } @@ -117,7 +137,7 @@ pub fn create_certificate( subject_alt_name: &ua::Array, cert_format: &ua::CertificateFormat, params: Option<&ua::KeyValueMap>, -) -> Result<(Certificate, PrivateKey)> { +) -> crate::Result<(Certificate, PrivateKey)> { // Create logger that forwards to Rust `log`. It is only used for the function call below and it // will be cleaned up at the end of the function. let mut logger = ua::Logger::rust_log(); diff --git a/src/traits.rs b/src/traits.rs index 547a302..ce51d8f 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -41,3 +41,16 @@ pub trait Attributes: DataType { /// Gets generic [`ua::NodeAttributes`] type. fn as_node_attributes(&self) -> &ua::NodeAttributes; } + +/// Custom certificate verification. +/// +/// This is used to implement custom callbacks in [`ua::CertificateVerification::custom()`]. +pub trait CustomCertificateVerification { + fn verify_certificate(&self, certificate: &ua::ByteString) -> ua::StatusCode; + + fn verify_application_uri( + &self, + certificate: &ua::ByteString, + application_uri: &ua::String, + ) -> ua::StatusCode; +} diff --git a/src/ua.rs b/src/ua.rs index ace938b..61d1958 100644 --- a/src/ua.rs +++ b/src/ua.rs @@ -5,6 +5,7 @@ mod array; mod browse_result_mask; #[cfg(feature = "mbedtls")] mod certificate_format; +mod certificate_verification; mod client; mod client_config; mod continuation_point; @@ -15,6 +16,7 @@ mod logger; mod monitored_item_id; mod node_class_mask; mod secure_channel_state; +mod security_level; mod server; mod server_config; mod session_state; @@ -28,6 +30,7 @@ pub use self::{ access_level::AccessLevel, array::Array, browse_result_mask::BrowseResultMask, + certificate_verification::CertificateVerification, client::{Client, ClientState}, continuation_point::ContinuationPoint, data_types::*, @@ -36,6 +39,7 @@ pub use self::{ monitored_item_id::MonitoredItemId, node_class_mask::NodeClassMask, secure_channel_state::SecureChannelState, + security_level::SecurityLevel, server::Server, session_state::SessionState, specified_attributes::SpecifiedAttributes, diff --git a/src/ua/array.rs b/src/ua/array.rs index 14cd2c3..a8a3442 100644 --- a/src/ua/array.rs +++ b/src/ua/array.rs @@ -1,7 +1,8 @@ use std::{ cmp, ffi::c_void, - fmt, mem, + fmt, + mem::{self, ManuallyDrop}, num::NonZeroUsize, ops, ptr::{self, NonNull}, @@ -340,12 +341,11 @@ impl Array { /// memory. Alternatively, they may be re-wrapped by [`from_raw_parts()`](Self::from_raw_parts). #[must_use] pub(crate) fn into_raw_parts(self) -> (usize, *mut T::Inner) { - // SAFETY: This gets the raw parts but we make sure to forget about `self` below, to prevent - // it from being released. This leaks the data until it is captured again. - let (size, ptr) = unsafe { self.as_raw_parts() }; - - // Make sure that `drop()` is not called anymore. - mem::forget(self); + // Use `ManuallyDrop` to avoid double-free even when added code might cause panic. See + // documentation of `mem::forget()` for details. + let this = ManuallyDrop::new(self); + // SAFETY: This gets the raw parts but we made sure that the destructor is not called again. + let (size, ptr) = unsafe { this.as_raw_parts() }; // Cast to `*mut T::Inner` because we no longer own the data. This is the only pointer to it // in existence. (size, ptr.cast_mut()) diff --git a/src/ua/certificate_verification.rs b/src/ua/certificate_verification.rs new file mode 100644 index 0000000..ca91970 --- /dev/null +++ b/src/ua/certificate_verification.rs @@ -0,0 +1,185 @@ +use std::{ + mem::{self, ManuallyDrop, MaybeUninit}, + ptr, +}; + +use open62541_sys::{ + UA_ByteString, UA_CertificateVerification, UA_CertificateVerification_AcceptAll, UA_StatusCode, + UA_String, +}; + +use crate::{ua, CustomCertificateVerification, DataType, Userdata}; + +/// Wrapper for [`UA_CertificateVerification`] from [`open62541_sys`]. +#[derive(Debug)] +pub struct CertificateVerification(UA_CertificateVerification); + +impl CertificateVerification { + /// Creates certificate verification with all checks disabled. + /// + /// Note that this disables certificate verification entirely. Use only when the other end can + /// be identified in some other way, or identity is not relevant. + #[must_use] + pub fn accept_all() -> Self { + let mut certificate_verification = Self::init(); + // SAFETY: Certificate verification is null, but that is valid. + unsafe { + UA_CertificateVerification_AcceptAll(certificate_verification.as_mut_ptr()); + } + certificate_verification + } + + /// Creates certificate verification with custom callbacks. + pub fn custom(certificate_verification: impl CustomCertificateVerification + 'static) -> Self { + type Ud = Userdata>; + + unsafe extern "C" fn verify_certificate_c( + cv: *const UA_CertificateVerification, + certificate: *const UA_ByteString, + ) -> UA_StatusCode { + // SAFETY: Reference is used only for the remainder of this function. + let certificate = ua::ByteString::raw_ref(unsafe { + certificate.as_ref().expect("certificate should be set") + }); + + // SAFETY: We use the user data only when it is still alive. + let certificate_verification = unsafe { Ud::peek_at((*cv).context) }; + let status_code = certificate_verification.verify_certificate(certificate); + status_code.into_raw() + } + + unsafe extern "C" fn verify_application_uri_c( + cv: *const UA_CertificateVerification, + certificate: *const UA_ByteString, + application_uri: *const UA_String, + ) -> UA_StatusCode { + // SAFETY: References are used only for the remainder of this function. + let certificate = ua::ByteString::raw_ref(unsafe { + certificate.as_ref().expect("certificate should be set") + }); + let application_uri = ua::String::raw_ref(unsafe { + application_uri + .as_ref() + .expect("application URI should be set") + }); + + // SAFETY: We use the user data only when it is still alive. + let certificate_verification = unsafe { Ud::peek_at((*cv).context) }; + let status_code = + certificate_verification.verify_application_uri(certificate, application_uri); + status_code.into_raw() + } + + unsafe extern "C" fn clear_c(cv: *mut UA_CertificateVerification) { + // Reclaim ownership of certificate verification and drop it. + // SAFETY: We use the user data only when it is still alive. + let _unused = unsafe { Ud::consume((*cv).context) }; + } + + let inner = UA_CertificateVerification { + context: Ud::prepare(Box::new(certificate_verification)), + verifyCertificate: Some(verify_certificate_c), + verifyApplicationURI: Some(verify_application_uri_c), + getExpirationDate: None, + getSubjectName: None, + clear: Some(clear_c), + logging: ptr::null_mut(), + }; + + unsafe { Self::from_raw(inner) } + } + + /// Creates wrapper by taking ownership of value. + /// + /// When `Self` is dropped, allocations held by the inner type are cleaned up. + /// + /// # Safety + /// + /// Ownership of the value passes to `Self`. This must only be used for values that are not + /// contained within other values that may be dropped. + #[must_use] + pub(crate) const unsafe fn from_raw(src: UA_CertificateVerification) -> Self { + Self(src) + } + + /// Gives up ownership and returns value. + /// + /// The returned value must be re-wrapped with [`from_raw()`], cleared manually, or copied into + /// an owning value (like [`UA_Client`]) to free internal allocations and not leak memory. + /// + /// [`from_raw()`]: Self::from_raw + /// [`UA_Client`]: open62541_sys::UA_Client + #[must_use] + pub(crate) fn into_raw(self) -> UA_CertificateVerification { + // Use `ManuallyDrop` to avoid double-free even when added code might cause panic. See + // documentation of `mem::forget()` for details. + let this = ManuallyDrop::new(self); + // SAFETY: Aliasing memory temporarily is safe because destructor will not be called. + unsafe { ptr::read(ptr::addr_of!(this.0)) } + } + + /// Creates wrapper initialized with defaults. + /// + /// This initializes the value and makes all attributes well-defined. Additional attributes may + /// need to be initialized for the value to be actually useful afterwards. + pub(crate) const fn init() -> Self { + let inner = MaybeUninit::::zeroed(); + // SAFETY: Zero-initialized memory is a valid certificate verification. + let inner = unsafe { inner.assume_init() }; + // SAFETY: We pass a value without pointers to it into `Self`. + unsafe { Self::from_raw(inner) } + } + + /// Moves value into `dst`, giving up ownership. + /// + /// Existing data in `dst` is cleared before moving the value; it is safe to use this operation + /// on already initialized target values. + /// + /// The logging reference will be transferred from the old to the new certificate verification. + /// + /// After this, it is the responsibility of `dst` to eventually clean up the data. + pub(crate) fn move_into_raw(self, dst: &mut UA_CertificateVerification) { + // Move certificate verification into target, transferring ownership. + let orig = mem::replace(dst, self.into_raw()); + // Take ownership of previously set certificate verification in order to drop it. + let mut orig = unsafe { Self::from_raw(orig) }; + // Before dropping, transfer previously set logging to new certificate verification. We do + // this because certificate verifications do not own the logging reference. For instance, + // after creating a new config, the config's owned logger is copied (!) here. Refer to + // comments in `ClientConfig::new()` for more info. + mem::swap(&mut dst.logging, &mut unsafe { orig.as_mut() }.logging); + } + + /// Returns exclusive reference to value. + /// + /// # Safety + /// + /// The value is owned by `Self`. Ownership must not be given away, in whole or in parts. This + /// may happen when `open62541` functions are called that take ownership of values by pointer. + #[allow(dead_code)] // This is unused for now. + #[must_use] + pub(crate) unsafe fn as_mut(&mut self) -> &mut UA_CertificateVerification { + &mut self.0 + } + + /// Returns mutable pointer to value. + /// + /// # Safety + /// + /// The value is owned by `Self`. Ownership must not be given away, in whole or in parts. This + /// may happen when `open62541` functions are called that take ownership of values by pointer. + #[must_use] + pub(crate) unsafe fn as_mut_ptr(&mut self) -> *mut UA_CertificateVerification { + ptr::addr_of_mut!(self.0) + } +} + +impl Drop for CertificateVerification { + fn drop(&mut self) { + if let Some(clear) = self.0.clear { + unsafe { + clear(ptr::addr_of_mut!(self.0)); + } + } + } +} diff --git a/src/ua/data_types.rs b/src/ua/data_types.rs index 0c8455d..a2343c7 100644 --- a/src/ua/data_types.rs +++ b/src/ua/data_types.rs @@ -30,9 +30,11 @@ mod delete_monitored_items_request; mod delete_monitored_items_response; mod delete_subscriptions_request; mod delete_subscriptions_response; +mod endpoint_description; mod expanded_node_id; mod extension_object; mod localized_text; +mod message_security_mode; mod monitored_item_create_request; mod monitored_item_create_result; mod node_attributes; @@ -86,9 +88,11 @@ pub use self::{ delete_monitored_items_response::DeleteMonitoredItemsResponse, delete_subscriptions_request::DeleteSubscriptionsRequest, delete_subscriptions_response::DeleteSubscriptionsResponse, + endpoint_description::EndpointDescription, expanded_node_id::ExpandedNodeId, extension_object::ExtensionObject, localized_text::LocalizedText, + message_security_mode::MessageSecurityMode, monitored_item_create_request::MonitoredItemCreateRequest, monitored_item_create_result::MonitoredItemCreateResult, node_attributes::{ diff --git a/src/ua/data_types/application_description.rs b/src/ua/data_types/application_description.rs index 6e5dcb5..3240ce7 100644 --- a/src/ua/data_types/application_description.rs +++ b/src/ua/data_types/application_description.rs @@ -93,4 +93,39 @@ impl ApplicationDescription { .move_into_raw(&mut self.0.discoveryUrlsSize, &mut self.0.discoveryUrls); self } + + #[must_use] + pub fn application_uri(&self) -> &ua::String { + ua::String::raw_ref(&self.0.applicationUri) + } + + #[must_use] + pub fn product_uri(&self) -> &ua::String { + ua::String::raw_ref(&self.0.productUri) + } + + #[must_use] + pub fn application_name(&self) -> &ua::LocalizedText { + ua::LocalizedText::raw_ref(&self.0.applicationName) + } + + #[must_use] + pub fn application_type(&self) -> &ua::ApplicationType { + ua::ApplicationType::raw_ref(&self.0.applicationType) + } + + #[must_use] + pub fn gateway_server_uri(&self) -> &ua::String { + ua::String::raw_ref(&self.0.gatewayServerUri) + } + + #[must_use] + pub fn discovery_profile_uri(&self) -> &ua::String { + ua::String::raw_ref(&self.0.discoveryProfileUri) + } + + #[must_use] + pub fn discovery_urls(&self) -> Option<&[ua::String]> { + unsafe { ua::Array::slice_from_raw_parts(self.0.discoveryUrlsSize, self.0.discoveryUrls) } + } } diff --git a/src/ua/data_types/endpoint_description.rs b/src/ua/data_types/endpoint_description.rs new file mode 100644 index 0000000..984a698 --- /dev/null +++ b/src/ua/data_types/endpoint_description.rs @@ -0,0 +1,40 @@ +use crate::{ua, DataType}; + +crate::data_type!(EndpointDescription); + +impl EndpointDescription { + #[must_use] + pub fn endpoint_url(&self) -> &ua::String { + ua::String::raw_ref(&self.0.endpointUrl) + } + + #[must_use] + pub fn server(&self) -> &ua::ApplicationDescription { + ua::ApplicationDescription::raw_ref(&self.0.server) + } + + #[must_use] + pub fn server_certificate(&self) -> &ua::ByteString { + ua::ByteString::raw_ref(&self.0.serverCertificate) + } + + #[must_use] + pub fn security_mode(&self) -> &ua::MessageSecurityMode { + ua::MessageSecurityMode::raw_ref(&self.0.securityMode) + } + + #[must_use] + pub fn security_policy_uri(&self) -> &ua::String { + ua::String::raw_ref(&self.0.securityPolicyUri) + } + + #[must_use] + pub fn transport_profile_uri(&self) -> &ua::String { + ua::String::raw_ref(&self.0.transportProfileUri) + } + + #[must_use] + pub const fn security_level(&self) -> ua::SecurityLevel { + ua::SecurityLevel::new(self.0.securityLevel) + } +} diff --git a/src/ua/data_types/message_security_mode.rs b/src/ua/data_types/message_security_mode.rs new file mode 100644 index 0000000..9049897 --- /dev/null +++ b/src/ua/data_types/message_security_mode.rs @@ -0,0 +1,7 @@ +crate::data_type!(MessageSecurityMode, UInt32); + +crate::enum_variants!( + MessageSecurityMode, + UA_MessageSecurityMode, + [INVALID, NONE, SIGN, SIGNANDENCRYPT] +); diff --git a/src/ua/key_value_map.rs b/src/ua/key_value_map.rs index 3006fc0..b534c4c 100644 --- a/src/ua/key_value_map.rs +++ b/src/ua/key_value_map.rs @@ -1,4 +1,4 @@ -use std::{mem, ptr::NonNull}; +use std::{mem::ManuallyDrop, ptr::NonNull}; use open62541_sys::{ UA_KeyValueMap, UA_KeyValueMap_contains, UA_KeyValueMap_delete, UA_KeyValueMap_get, @@ -116,11 +116,12 @@ impl KeyValueMap { /// Gives up ownership and returns value. #[allow(dead_code)] // This is unused for now. - pub(crate) const fn into_inner(self) -> *mut UA_KeyValueMap { - let key_value_map = self.0.as_ptr(); - // Make sure that `drop()` is not called anymore. - mem::forget(self); - key_value_map + pub(crate) fn into_raw(self) -> *mut UA_KeyValueMap { + // Use `ManuallyDrop` to avoid double-free even when added code might cause panic. See + // documentation of `mem::forget()` for details. + let this = ManuallyDrop::new(self); + // Return pointer to caller who becomes the owner of the object. + this.0.as_ptr() } /// Returns const pointer to value. diff --git a/src/ua/logger.rs b/src/ua/logger.rs index 73b3176..29faa7d 100644 --- a/src/ua/logger.rs +++ b/src/ua/logger.rs @@ -1,6 +1,6 @@ mod rust_log; -use std::{mem, ptr::NonNull}; +use std::{mem::ManuallyDrop, ptr::NonNull}; use open62541_sys::UA_Logger; @@ -31,11 +31,12 @@ impl Logger { } /// Gives up ownership and returns value. - pub(crate) const fn into_raw(self) -> *mut UA_Logger { - let logger = self.0.as_ptr(); - // Make sure that `drop()` is not called anymore. - mem::forget(self); - logger + pub(crate) fn into_raw(self) -> *mut UA_Logger { + // Use `ManuallyDrop` to avoid double-free even when added code might cause panic. See + // documentation of `mem::forget()` for details. + let this = ManuallyDrop::new(self); + // Return pointer to caller who becomes the owner of the object. + this.0.as_ptr() } /// Returns mutable pointer to value. diff --git a/src/ua/security_level.rs b/src/ua/security_level.rs new file mode 100644 index 0000000..9bc2f03 --- /dev/null +++ b/src/ua/security_level.rs @@ -0,0 +1,23 @@ +use open62541_sys::UA_Byte; + +use crate::ua; + +/// Wrapper for security level from [`open62541_sys`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SecurityLevel(UA_Byte); + +impl SecurityLevel { + pub(crate) const fn new(security_level: UA_Byte) -> Self { + Self(security_level) + } + + #[allow(dead_code)] // This is unused for now. + pub(crate) const fn as_u8(self) -> u8 { + self.0 + } + + #[allow(dead_code)] // This is unused for now. + pub(crate) const fn to_byte(self) -> ua::Byte { + ua::Byte::new(self.as_u8()) + } +}