Skip to content

Add support for the zeroize crate#71

Merged
djc merged 1 commit intorustls:mainfrom
jarhodes314:feat/zeroize
Feb 5, 2025
Merged

Add support for the zeroize crate#71
djc merged 1 commit intorustls:mainfrom
jarhodes314:feat/zeroize

Conversation

@jarhodes314
Copy link
Contributor

@jarhodes314 jarhodes314 commented Feb 5, 2025

With a previous version of rustls, we were using zeroize to ensure that private keys were destroyed in memory once we were no longer using them. This PR adds a zeroize feature flag which enables support for the zeroize crate, implementing zeroization logic for PrivateKeyDer<'static> where the underlying data is BytesInner::Owned.

If the underlying data is borrowed (i.e. &'static [u8]), attempting to zeroize this will (in debug mode) panic since there is nothing we can do to zeroize the data. This shouldn't be a problem since zeroization is opt-in from the consumer, as well as the fact that there is no good reason I can think of why someone would want to store a private key in a static or Box::leak such data when they can easily store it as a vector. If the underlying data is borrowed, this will do nothing as there is nothing the code can usefully do at this point (and it's highly unlikely anyone will end up in such a situation themselves).

I have added some tests for this feature to check that it panics/does not panic under the appropriate conditions. I've not tested for the actual zeroization as I don't want to be testing the zeroize crate itself.

Copy link
Member

@djc djc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this makes sense to me!

Cargo.toml Outdated
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
web-time = { version = "1", optional = true }

[dependencies]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this before the platform-specific dependencies sections?

Cargo.toml Outdated
alloc = []
std = ["alloc"]
web = ["web-time"]
zeroize = ["alloc", "dep:zeroize"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to enable this by default?

@ctz
Copy link
Member

ctz commented Feb 5, 2025

I would be minded to make this mandatory, since this crate is only really used for rustls which already has a mandatory dependency on zeroize. (Equivalently, we could unconditionally enable the pki-types feature in rustls, but that seems like an extra step to the same result.)

Not sure about the panic behaviour -- it makes sense if you get one from a &[u8] but not if you end up with (say) a PrivateKeyDer<'a> (which panics on drop) borrowing from a PrivateKeyDer<'static> (which zeroizes on drop). But I don't think we can express that in these types.

@jarhodes314
Copy link
Contributor Author

I would be minded to make this mandatory, since this crate is only really used for rustls which already has a mandatory dependency on zeroize. (Equivalently, we could unconditionally enable the pki-types feature in rustls, but that seems like an extra step to the same result.)

That makes sense. I think it probably wants to be guarded by the alloc feature since there is nothing this implementation can usefully do without alloc enabled?

Not sure about the panic behaviour -- it makes sense if you get one from a &[u8] but not if you end up with (say) a PrivateKeyDer<'a> (which panics on drop) borrowing from a PrivateKeyDer<'static> (which zeroizes on drop). But I don't think we can express that in these types.

Yeah, I'm not terribly well versed in how those interactions play out, but you're probably right. I think it's an edge case that's extremely unlikely to pop up anyway, so it's unlikely to be helping anyone (and there is a slim chance it may negatively impact someone).

@djc
Copy link
Member

djc commented Feb 5, 2025

(Mind squashing your changes?)

@jarhodes314
Copy link
Contributor Author

(Mind squashing your changes?)

Done

Copy link
Member

@cpu cpu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks :)

@djc djc enabled auto-merge February 5, 2025 14:19
@djc djc added this pull request to the merge queue Feb 5, 2025
Merged via the queue into rustls:main with commit e7c38ac Feb 5, 2025
42 checks passed
@dare3path
Copy link

Hi, sorry to bother, a question: will this change be in a version like 1.12.0 or like 1.11.1 ? I'm only wondering which will be incremented, the minor or the patch. Current latest version being 1.11.0.

Thanks.

@djc
Copy link
Member

djc commented Apr 25, 2025

I guess it should be 1.12.0? It technically adds public API.

@dare3path
Copy link

dare3path commented Apr 25, 2025

For clarity, this PR did not add zeroization on Drop, right? I wrongly assumed it did, but instead it only added the zeroize method.

If it's true that zeroize wasn't called somehow on drop, then I've had to implement it by doing something like the following(with Grok 3) because I wanted to trace it to see if it's working(rather than derive it):

diff/patch (click me to expand)

to be applied on commit b59e08d (HEAD -> main, origin/main, origin/HEAD)

diff --git a/src/lib.rs b/src/lib.rs
index c95369f..50e0004 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -134,6 +134,8 @@ pub enum PrivateKeyDer<'a> {
 #[cfg(feature = "alloc")]
 impl zeroize::Zeroize for PrivateKeyDer<'static> {
     fn zeroize(&mut self) {
+        #[cfg(feature="std")]
+        eprintln!("!!!! calling zeroize on PrivateKeyDer");
         match self {
             Self::Pkcs1(key) => key.zeroize(),
             Self::Sec1(key) => key.zeroize(),
@@ -142,6 +144,64 @@ impl zeroize::Zeroize for PrivateKeyDer<'static> {
     }
 }
 
+#[cfg(feature = "alloc")]
+impl zeroize::ZeroizeOnDrop for PrivateKeyDer<'static> {} // Explicit marker
+
+impl<'a> Drop for PrivateKeyDer<'a> {
+    fn drop(&mut self) {
+        //use zeroize::Zeroize;
+        match self {
+            Self::Pkcs1(key) => {
+                if let BytesInner::Owned(vec) = &mut key.0.0 {
+                    #[cfg(feature="std")]
+                    eprintln!("!!!! Dropping PrivateKeyDer::Pkcs1");
+                    //vec.zeroize();
+                    //<Vec<u8> as zeroize::Zeroize>::zeroize(vec);
+                    //zeroize::Zeroize::zeroize(vec);
+                    zeroize_vec(vec);
+                }
+            }
+            Self::Sec1(key) => {
+                if let BytesInner::Owned(vec) = &mut key.0.0 {
+                    #[cfg(feature="std")]
+                    eprintln!("!!!! Dropping PrivateKeyDer::Sec1");
+                    //vec.zeroize();
+                    //<Vec<u8> as zeroize::Zeroize>::zeroize(vec);
+                    //zeroize::Zeroize::zeroize(vec);
+                    zeroize_vec(vec);
+                }
+            }
+            Self::Pkcs8(key) => {
+                if let BytesInner::Owned(vec) = &mut key.0.0 {
+                    #[cfg(feature="std")]
+                    eprintln!("!!!! Dropping PrivateKeyDer::Pkcs8");
+                    //vec.zeroize();
+                    //<Vec<u8> as zeroize::Zeroize>::zeroize(vec);
+                    //zeroize::Zeroize::zeroize(vec);
+                    zeroize_vec(vec);
+                }
+            }
+        }
+    }
+}
+
+#[cfg(feature = "alloc")]
+fn zeroize_vec(vec: &mut Vec<u8>) {
+    // Check if non-zero before
+    #[cfg(feature="std")] {
+    let has_non_zero = vec.iter().any(|&b| b != 0);
+    eprintln!("Zeroizing Vec<u8>, has non-zero bytes: {}", has_non_zero);
+    }
+    zeroize::Zeroize::zeroize(vec);
+    // Verify all zeros after
+    #[cfg(feature="std")] {
+    let is_all_zeros = vec.iter().all(|&b| b == 0);
+    eprintln!("After zeroize, Vec<u8> is all zeros: {}", is_all_zeros);
+    }
+}
+
+
+
 impl PrivateKeyDer<'_> {
     /// Clone the private key to a `'static` value
     #[cfg(feature = "alloc")]
@@ -283,10 +343,19 @@ impl TryFrom<Vec<u8>> for PrivateKeyDer<'_> {
     type Error = &'static str;
 
     fn try_from(key: Vec<u8>) -> Result<Self, Self::Error> {
-        Ok(match PrivateKeyDer::try_from(&key[..])? {
-            PrivateKeyDer::Pkcs1(_) => Self::Pkcs1(key.into()),
-            PrivateKeyDer::Sec1(_) => Self::Sec1(key.into()),
-            PrivateKeyDer::Pkcs8(_) => Self::Pkcs8(key.into()),
+        let variant = {
+            let result = PrivateKeyDer::try_from(&key[..])?;
+            match result {
+                PrivateKeyDer::Pkcs1(_) => 0,
+                PrivateKeyDer::Sec1(_) => 1,
+                PrivateKeyDer::Pkcs8(_) => 2,
+            }
+        };
+        Ok(match variant {
+            0 => Self::Pkcs1(key.into()),
+            1 => Self::Sec1(key.into()),
+            2 => Self::Pkcs8(key.into()),
+            _ => unreachable!(),
         })
     }
 }
@@ -327,10 +396,26 @@ impl PrivatePkcs1KeyDer<'_> {
 #[cfg(feature = "alloc")]
 impl zeroize::Zeroize for PrivatePkcs1KeyDer<'static> {
     fn zeroize(&mut self) {
+        #[cfg(feature="std")]
+        eprintln!("!!!! calling zeroize on PrivatePkcs1KeyDer");
         self.0.0.zeroize()
     }
 }
 
+#[cfg(feature = "alloc")]
+impl zeroize::ZeroizeOnDrop for PrivatePkcs1KeyDer<'static> {} // Explicit marker
+
+#[cfg(feature = "alloc")]
+impl<'a> Drop for PrivatePkcs1KeyDer<'a> {
+    fn drop(&mut self) {
+        if let BytesInner::Owned(vec) = &mut self.0.0 {
+            #[cfg(feature="std")]
+            eprintln!("!!!! Dropping PrivatePkcs1KeyDer");
+            zeroize_vec(vec);
+        }
+    }
+}
+
 #[cfg(feature = "alloc")]
 impl PemObjectFilter for PrivatePkcs1KeyDer<'static> {
     const KIND: SectionKind = SectionKind::RsaPrivateKey;
@@ -394,10 +479,26 @@ impl PrivateSec1KeyDer<'_> {
 #[cfg(feature = "alloc")]
 impl zeroize::Zeroize for PrivateSec1KeyDer<'static> {
     fn zeroize(&mut self) {
+        #[cfg(feature="std")]
+        eprintln!("!!!! calling zeroize on PrivateSec1KeyDer");
         self.0.0.zeroize()
     }
 }
 
+#[cfg(feature = "alloc")]
+impl zeroize::ZeroizeOnDrop for PrivateSec1KeyDer<'static> {} // Explicit marker
+
+#[cfg(feature = "alloc")]
+impl<'a> Drop for PrivateSec1KeyDer<'a> {
+    fn drop(&mut self) {
+        if let BytesInner::Owned(vec) = &mut self.0.0 {
+            #[cfg(feature="std")]
+            eprintln!("!!!! Dropping PrivateSec1KeyDer");
+            zeroize_vec(vec);
+        }
+    }
+}
+
 #[cfg(feature = "alloc")]
 impl PemObjectFilter for PrivateSec1KeyDer<'static> {
     const KIND: SectionKind = SectionKind::EcPrivateKey;
@@ -462,10 +563,29 @@ impl PrivatePkcs8KeyDer<'_> {
 #[cfg(feature = "alloc")]
 impl zeroize::Zeroize for PrivatePkcs8KeyDer<'static> {
     fn zeroize(&mut self) {
+        #[cfg(feature="std")]
+        eprintln!("!!!! calling zeroize on PrivatePkcs8KeyDer");
         self.0.0.zeroize()
     }
 }
 
+#[cfg(feature = "alloc")]
+impl zeroize::ZeroizeOnDrop for PrivatePkcs8KeyDer<'static> {} // Explicit marker
+
+#[cfg(feature = "alloc")]
+impl<'a> Drop for PrivatePkcs8KeyDer<'a> {
+    fn drop(&mut self) {
+        #[cfg(feature="std")]
+        eprintln!("!!!! Dropping PrivatePkcs8KeyDer");
+        if let BytesInner::Owned(vec) = &mut self.0.0 {
+            #[cfg(feature="std")]
+            eprintln!("!!!! zeroing PrivatePkcs8KeyDer");
+            //zeroize::Zeroize::zeroize(vec);
+            zeroize_vec(vec);
+        }
+    }
+}
+
 #[cfg(feature = "alloc")]
 impl PemObjectFilter for PrivatePkcs8KeyDer<'static> {
     const KIND: SectionKind = SectionKind::PrivateKey;
@@ -1027,8 +1147,17 @@ enum BytesInner<'a> {
 #[cfg(feature = "alloc")]
 impl BytesInner<'_> {
     fn into_owned(self) -> BytesInner<'static> {
+//        BytesInner::Owned(match self {
+//            Self::Owned(vec) => vec, // original line, won't work due to our Drop impl.
+//            Self::Borrowed(slice) => slice.to_vec(),
+//        })
         BytesInner::Owned(match self {
-            Self::Owned(vec) => vec,
+            Self::Owned(ref vec) => {
+                #[cfg(feature="std")]
+                eprintln!("!!! making a BytesInner's vec clone.");
+                //FIXME: need a better way
+                vec.clone()
+            },
             Self::Borrowed(slice) => slice.to_vec(),
         })
     }
@@ -1038,12 +1167,36 @@ impl BytesInner<'_> {
 impl zeroize::Zeroize for BytesInner<'static> {
     fn zeroize(&mut self) {
         match self {
-            BytesInner::Owned(vec) => vec.zeroize(),
-            BytesInner::Borrowed(_) => (),
+            BytesInner::Owned(vec) => {
+                #[cfg(feature="std")]
+                eprintln!("!!!! calling zeroize on BytesInner::Owned");
+                vec.zeroize()
+            },
+            BytesInner::Borrowed(_) => {
+                #[cfg(feature="std")]
+                eprintln!("!!!! NOT calling zeroize on BytesInner::Borrowed");
+                ()
+            },
         }
     }
 }
 
+#[cfg(feature = "alloc")]
+impl zeroize::ZeroizeOnDrop for BytesInner<'static> {} // Explicit marker
+
+#[cfg(feature = "alloc")]
+impl<'a> Drop for BytesInner<'a> {
+    fn drop(&mut self) {
+        //zeroize::Zeroize::zeroize(self);// won't work due to lifetimes
+        if let BytesInner::Owned(vec) = self {
+            #[cfg(feature="std")]
+            eprintln!("!!!! Dropping BytesInner");
+            zeroize_vec(vec);
+        }
+    }
+}
+
+
 impl AsRef<[u8]> for BytesInner<'_> {
     fn as_ref(&self) -> &[u8] {
         match &self {
@@ -1118,4 +1271,15 @@ mod tests {
         assert_ne!(owned_a, borrowed_b);
         assert_ne!(borrowed_b, owned_a);
     }
+
+}
+
+#[test]
+fn test_zeroize() {
+    const fn assert_zeroize<T: zeroize::Zeroize + zeroize::ZeroizeOnDrop>() {}
+    assert_zeroize::<PrivatePkcs8KeyDer<'static>>();
+    assert_zeroize::<PrivateKeyDer<'static>>();
+    assert_zeroize::<PrivatePkcs1KeyDer<'static>>();
+    assert_zeroize::<PrivateSec1KeyDer<'static>>();
+    assert_zeroize::<BytesInner<'static>>();
 }

EDIT: I'll be doing any updates to the above diff, here: https://github.com/dare3path/zeroize-test2/blob/master/zeroize_on_drop.patch

@djc djc mentioned this pull request May 1, 2025
@djc
Copy link
Member

djc commented May 1, 2025

@dare3path you're right. Fixing that in #79.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants