Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Implement memory zeroization before dropping private key #186

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Zeroize memory held by `PrivateKey` when dropped.

## [0.7.0] - 2024-12-13

### Added
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tokio = { version = "1.35.1", optional = true, features = [
"time",
] }
x509-certificate = { version = "0.24.0", optional = true }
zeroize = { version = "1.8.1", optional = true }

[dev-dependencies]
anyhow = "1.0.79"
Expand All @@ -47,7 +48,7 @@ tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] }

[features]
default = ["serde", "time", "tokio"]
mbedtls = ["open62541-sys/mbedtls"]
mbedtls = ["dep:zeroize", "open62541-sys/mbedtls"]
serde = ["dep:serde", "dep:serde_json", "time?/formatting", "time?/serde"]
time = ["dep:time"]
tokio = ["dep:tokio"]
Expand Down
15 changes: 11 additions & 4 deletions src/ssl.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{fmt, ptr};

use open62541_sys::UA_CreateCertificate;
use zeroize::Zeroizing;

use crate::{ua, DataType, Error};

Expand Down Expand Up @@ -63,14 +64,17 @@ impl Certificate {

/// Private key in [DER] or [PEM] format.
///
/// The wrapped memory is [zeroized] when dropped.
///
/// [DER]: https://en.wikipedia.org/wiki/X.690#DER_encoding
/// [PEM]: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail
/// [zeroized]: https://crates.io/crates/zeroize
#[derive(Clone)]
pub struct PrivateKey(ua::ByteString);
pub struct PrivateKey(Zeroizing<ua::ByteString>);

impl PrivateKey {
pub(crate) fn from_byte_string(byte_string: ua::ByteString) -> Option<Self> {
(!byte_string.is_invalid()).then(|| Self(byte_string))
(!byte_string.is_invalid()).then(|| Self(Zeroizing::new(byte_string)))
}

pub(crate) unsafe fn from_string_unchecked(string: ua::String) -> Self {
Expand All @@ -81,9 +85,12 @@ impl PrivateKey {
///
/// This does not validate the data. When passing the instance to another method, that method
/// may still fail if the private key is not valid.
///
/// As this takes only a slice and returns an owned copy, the original data should be zeroized
/// independently as soon as it is no longer needed.
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Self {
Self(ua::ByteString::new(bytes))
Self(Zeroizing::new(ua::ByteString::new(bytes)))
}

/// Gets certificate data.
Expand All @@ -93,7 +100,7 @@ impl PrivateKey {
unsafe { self.0.as_bytes_unchecked() }
}

pub(crate) const fn as_byte_string(&self) -> &ua::ByteString {
pub(crate) fn as_byte_string(&self) -> &ua::ByteString {
&self.0
}
}
Expand Down
61 changes: 60 additions & 1 deletion src/ua/data_types/byte_string.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::slice;

use open62541_sys::{UA_ByteString_copy, UA_String};
use open62541_sys::{UA_ByteString_clear, UA_ByteString_copy, UA_ByteString_memZero, UA_String};

use crate::{ua, ArrayValue, DataType};

Expand Down Expand Up @@ -38,6 +38,24 @@ impl ByteString {
dst
}

#[allow(dead_code)] // --no-default-features
sgoll marked this conversation as resolved.
Show resolved Hide resolved
fn clear(&mut self) {
unsafe {
// Clearing frees the referenced heap memory and resets length and data pointer to all
// zeroes, i.e. the string becomes an "invalid" string (as defined by OPC UA).
UA_ByteString_clear(self.as_mut_ptr());
}
}

#[allow(dead_code)] // --no-default-features
fn mem_zero(&mut self) {
unsafe {
// This zeroizes the string contents, i.e. characters, leaving the string object itself
// intact. The string has the same length as before but is all `\0`.
UA_ByteString_memZero(self.as_mut_ptr());
}
}

/// Checks if byte string is invalid.
///
/// The invalid state is defined by OPC UA. It is a third state which is distinct from empty and
Expand Down Expand Up @@ -98,3 +116,44 @@ impl serde::Serialize for ByteString {
.and_then(|bytes| serializer.serialize_bytes(bytes))
}
}

#[cfg(feature = "mbedtls")]
impl zeroize::Zeroize for ua::ByteString {
fn zeroize(&mut self) {
// Clear the heap memory of the string characters.
self.mem_zero();
// Clear the string data structure, i.e. length.
self.clear();
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn zeroizing() {
let mut string = ua::ByteString::new("SECRET".as_bytes());
let data_ptr = string.as_bytes().unwrap().as_ptr();
let object_ptr = unsafe { string.as_ptr() };

let data = unsafe { slice::from_raw_parts(data_ptr, 6) };
assert_eq!(data, "SECRET".as_bytes());
let object = unsafe { std::ptr::read(object_ptr) };
assert!(!object.data.is_null());
assert_eq!(object.length, 6);

string.mem_zero();

// SAFETY: Memory has been zeroized but is still allocated.
let data = unsafe { slice::from_raw_parts(data_ptr, 6) };
assert_eq!(data, "\0\0\0\0\0\0".as_bytes());

string.clear();

// SAFETY: Object has been reset but is still allocated.
let object = unsafe { std::ptr::read(object_ptr) };
assert!(object.data.is_null());
assert_eq!(object.length, 0);
}
}
Loading