Skip to content

Share parsed mbedtls_x509_crt and mbedtls_pk_context between sessions #75

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

AnthonyGrondin
Copy link
Collaborator

Status: Still work in progress

The main intent behind this PR is to save memory allocated per session by re­using resources that could be shared between sessions, such as certificates.

This adds two new wrapper structs to initiate and parse X509 certs and Private keys, and manage the cleanup when they are dropped. Session now takes Certificates by reference so the same set of Certificates can be shared between sessions.

Also Certificates have been refactored to parse during init, and to contain the pointer to the allocated memory. One of my long term goal would be to be able to fetch data from the parsed certificates such as the expiring date, without needing extra external dependencies. The config struct is another few 200 bytes that could be saved by sharing it per session, but this would need to keep in memory what parameters were used. Or extend the Certificates builder mecanism so that Certificates own that config.

fn default() -> Self {
Self::new()
}
}

impl Certificates<'_> {
impl Certificates {
/// Create a new instance of [Certificates] with no certificates whatsoever
pub const fn new() -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this a real use-case? Can the library actually operate without any certs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added it to deal with the builder pattern. I'm still not sure if it's the best way, considering if we have a copy and no copy variant, with different lifetimes, I don't know how can we reconcile both functionalities under the same builder.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The lifetimes are covariant though so a single lifetime would do. If you want to keep the builder pattern, new would return a Certificates<'static> which would either stay like that (copy) or would be changed to Certificates<'a> by a builder non-copy method, where 'a would be a generic lifetime capturing the actual x509 lifetime (which can still end up being static after all).

@@ -265,7 +386,7 @@ pub struct Certificates<'a> {
/// that will be used to verify the client's certificate during the handshake.
/// When set to [None] the server will not request nor perform any verification
/// on the client certificates. Only set when you want to use client authentication.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"only set this when you want to use client authentication".
Hmm, is this correct? My understanding is that if you are running a TLS client (say - an HTTPS client), you might still need a bunch of CA certificates so as to verify the certificate that the server is providing to you? I don't think the server is obliged at all to give you the CA cert, it only gives you its own cert signed by the CA cert but then the CA cert (in fact - a whole bundle of these) needs to be configured in the client?

Where I'm going with that is that both client and server (the server needs it when client auth is enabled) need (optional) access to something that ESP-IDF calls a "certificate bundle" which is - to oversimplify - a X509[] array which is just a bunch of (CA or interim) certificates.

I think we are kind of missing the whole notion of a "certificate bundle" actually... Also, the certificate bundle is always the same and shared across everything, so perhaps it needs to be modeled with its own type. CertificatesBundle or something.

Copy link
Collaborator Author

@AnthonyGrondin AnthonyGrondin May 20, 2025

Choose a reason for hiding this comment

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

This seems to be a documentation error. It is indeed required nevertheless, and the optional certificate part for a server should actually be for server certficates, and not CA chain.

Currently X509 seems to support passing multiple chained certificates in the same parsing to create a bundle, but we might also extend the API to allow passing in a list of X509[] if the need is there.

EDIT: Although I'm confused because in mTLS examples we "enable" mTLS by adding a CA Chain. But this might be due to:

mbedtls_ssl_conf_authmode(
ssl_config,
if self.ca_chain.is_some() {
MBEDTLS_SSL_VERIFY_REQUIRED as i32
} else {
// Use this config when in server mode
// Ref: https://os.mbed.com/users/markrad/code/mbedtls/docs/tip/ssl_8h.html#a5695285c9dbfefec295012b566290f37
MBEDTLS_SSL_VERIFY_NONE as i32
},
);

Copy link
Collaborator

Choose a reason for hiding this comment

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

Currently X509 seems to support passing multiple chained certificates in the same parsing to create a bundle, but we might also extend the API to allow passing in a list of X509[] if the need is there.

My understanding is that a certificate chain (each cert in the chain signed by the following one) is a different thing from a bundle, which is a set of disparate root certs, as in google's, Mozilla's, verisign's etc. but I could be wrong.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You could be right. I don't have much knowledge on the bundle properties, and even less knowledge about how ESP-IDF implements it.

@AnthonyGrondin
Copy link
Collaborator Author

I added no-copy variants for parsing. I wish Rust could let us re-use the same function signature with different lifetimes requirements but its architecture doesn't allow it.

I also exposed the certificate verification mode to Session so that users can set it. It is optional because sane defaults exists for this setting. That being said, that makes the Session constructor pretty bloated, and I wonder if we should maybe switch to a builder pattern in a near-distant future. That would also avoid making a breaking API change every single iteration we try to change so feature around.

Currently client examples fail with verification failed so I have to figure out why that is. Server examples work fine.

@AnthonyGrondin
Copy link
Collaborator Author

So I'm still unsatisfied with the general API of this PR and general project. I refactored a proprietary app we're using, and I still have the following concerns:

  • Session::new() now has an ugly optional parameter in the middle of its argument, and it's a breaking change.
    We could either:

    1. Move the setting to Certificates, but this will block a user from using the same set of certificates with two different verification modes, and this is a session setting, not certificate setting.
    2. Provide a set_authmode() API to session to programatically change the authmode of mbedtls_ssl_config BUT Session:new already call init_ssl() which calls mbedtls_ssl_setup() with the config, and the documentation specifies

    No copy of the configuration context is made, it can be shared by many mbedtls_ssl_context structures.
    The conf structure will be accessed during the session. It must not be modified or freed as long as the session is active.

    So we cannot update the config once it has been used in a setup.

    1. Refactor Session to use a builder pattern, but this creates extra code and memory usage, and the goal is already to trim down both, so I consider this a regression in a way.
  • MbedTLSX509Crt and PkContext are still unexposed, and so, the user still doesn't have access to the parsed certificate and its parsed data, which was the original intent. My use case is that I want to be able to see the expiring date of a certificate without needing an extra dependency. It's already all available in there.

@ivmarkov
Copy link
Collaborator

ivmarkov commented May 22, 2025

  • Session::new() now has an ugly optional parameter in the middle of its argument

Why is it optional? What does passing None to auth_mode even mean, especially given that AuthMode::None also exists?

and it's a breaking change.

Worrying about breaking changes - especially at this minor level - is too optimistic IMO given the maturity level of the library. Also changing how Certificates operate, and even how they are passed (now by ref) means the breaking auth_mode change is the least of our concerns.

We could either:

  1. Move the setting to Certificates, but this will block a user from using the same set of certificates with two different verification modes, and this is a session setting, not certificate setting.
  2. Provide a set_authmode() API to session to programatically change the authmode of mbedtls_ssl_config BUT Session:new already call init_ssl() which calls mbedtls_ssl_setup() with the config, and the documentation specifies

Why are you leaning on builders so quickly? 3 or 4 or even 5 parameters provided at construction time is still OK IMO. Also this is embedded, so small compromises I think can be made in the name of efficiency. I would actually go with the auth_mode current setup, except for either making it non-optional if possible, or moving it to the end of the arguments list.

No copy of the configuration context is made, it can be shared by many mbedtls_ssl_context structures.
The conf structure will be accessed during the session. It must not be modified or freed as long as the session is active.

So we cannot update the config once it has been used in a setup.

  1. Refactor Session to use a builder pattern, but this creates extra code and memory usage, and the goal is already to trim down both, so I consider this a regression in a way.

As per above, I wouldn't bother.

  • MbedTLSX509Crt and PkContext are still unexposed, and so, the user still doesn't have access to the parsed certificate and its parsed data, which was the original intent. My use case is that I want to be able to see the expiring date of a certificate without needing an extra dependency. It's already all available in there.

Isn't this just a matter of putting in some extra code in Certificate so that you have access to the data you need? Is this related in any way to the other changes?

@AnthonyGrondin
Copy link
Collaborator Author

Why is it optional? What does passing None to auth_mode even mean, especially given that AuthMode::None also exists?

I agree that the current API is counter intuitive. Auth_mode should be optional because there are sane defaults for this setting which are MBEDTLS_SSL_VERIFY_NONE for server and MBEDTLS_SSL_VERIFY_REQUIRED for client. And these should be used by default for 90% of the time for security and usability purpose.

There's MBEDTLS_SSL_VERIFY_OPTIONAL that exists but since we don't expose mbedtls_ssl_get_verify_result() it's pretty much useless and the equivalent of MBEDTLS_SSL_VERIFY_NONE as-is, but it's still there.

A user would only really want to change these for:

  1. (Client) => use MBEDTLS_SSL_VERIFY_NONE to disable verification, but that doesn't work for TLS1.3 per my experimentations
  2. (Server) => use MBEDTLS_SSL_VERIFY_REQUIRED to request and force the peer to send client certificates to allow the connection.

Isn't this just a matter of putting in some extra code in Certificate so that you have access to the data you need? Is this related in any way to the other changes?

Yeah, I think we can use getters on Certificates to return the pointers directly, wrapped in options Option<*mut mbedtls_x509_crt> or Option<&MbedTLSX509Crt<'_>> for ca_chain and certificate

@ivmarkov
Copy link
Collaborator

Why is it optional? What does passing None to auth_mode even mean, especially given that AuthMode::None also exists?

I agree that the current API is counter intuitive. Auth_mode should be optional because there are sane defaults for this setting which are MBEDTLS_SSL_VERIFY_NONE for server and MBEDTLS_SSL_VERIFY_REQUIRED for client. And these should be used by default for 90% of the time for security and usability purpose.

There's MBEDTLS_SSL_VERIFY_OPTIONAL that exists but since we don't expose mbedtls_ssl_get_verify_result() it's pretty much useless and the equivalent of MBEDTLS_SSL_VERIFY_NONE as-is, but it's still there.

A user would only really want to change these for:

  1. (Client) => use MBEDTLS_SSL_VERIFY_NONE to disable verification, but that doesn't work for TLS1.3 per my experimentations
  2. (Server) => use MBEDTLS_SSL_VERIFY_REQUIRED to request and force the peer to send client certificates to allow the connection.

If we want a cleaner api, we might try fusing Mode and AuthMode into a single thing then, as to me it seems these are co-related and might be cleaner to express together?

@AnthonyGrondin
Copy link
Collaborator Author

Good idea. Perhaps a SessionConfig struct which would contain Mode, AuthMode and TlsVersion. If we ever add extra parameters, we can extend it

@AnthonyGrondin AnthonyGrondin force-pushed the feat/X509-memory-improvement branch from 11a02ed to 75d9920 Compare May 22, 2025 21:43
@AnthonyGrondin AnthonyGrondin marked this pull request as ready for review May 22, 2025 22:20
@AnthonyGrondin
Copy link
Collaborator Author

I'm getting pretty happy with the result so far.

  • Better lifetime management of X509 and Private key
  • Introduction of SessionConfig and user configurable AuthMode for better usability
  • Getters to fetch the parsed X509 properties
  • Certificate builder API that allows copy and no-copy variants.

One edge-case I found is that Certificates::with_certificates(&self, cert, private_key) which makes it easy to fulfill the requirement that both certificate and private key be used at the same time, or not, takes X509<'_> for both of its fields, requiring both the certificate and private key to be present in RAM at the same time.

I've calculated the memory cost difference of this to be about the size of the certificate slice, 2048KB in the example below.

That is:

-> Certificate loaded from RAM: 2048KB
-> Private key loaded from RAM: 2048KB
--- Total RAM usage: 4096KB
-> Copy parse: Certificates::with_certificates(&self, cert, private_key)
-> Allocation of `mbedtls_x509_crt` + parse: Figuratively 2048KB alloc
-> Allocation of `mbedtls_pk_context` + parse: Let's say 3000KB alloc
--- Total RAM usage: 9144KB
-> Drop of unused certificate and private key slice
---- Total RAM usage: 5048KB 

While if they were parsed one after the other, that would be something like:

-> Certificate loaded from RAM: 2048KB
-> Copy parse X509 certificate only
-> Allocation of `mbedtls_x509_crt` + parse: Figuratively 2048KB alloc
--- Total RAM usage: 4096KB
-> Drop unused certificate slice
--- Total RAM usage: 2048KB
-> Private key loaded from RAM: 2048KB
-> Copy parse of X509 private key
-> Allocation of `mbedtls_pk_context` + parse: Let's say 3000KB
--- Total RAM usage: 7096KB
-> Drop unused private key slice
---- Total RAM usage: 5048KB 

If a user really wants to save those bytes, they can use the nocopy variant.

@ivmarkov
Copy link
Collaborator

I've added feedback regarding the lifetimes of Certtificates on Session::new.

Also, I don't see why we should delay replacing aligned_alloc and free with mbedtls_calloc and mbedtls_free. We should no longer be using the global heap (neither the Rust one, nor the C one) in any of esp-mbedtls. Or else - and at the very least - I cannot make use of esp-mbedtls inside openthread.

@AnthonyGrondin AnthonyGrondin changed the title wip: Share parsed mbedtls_x509_crt and mbedtls_pk_context between sessions Share parsed mbedtls_x509_crt and mbedtls_pk_context between sessions May 23, 2025
@AnthonyGrondin
Copy link
Collaborator Author

Thank you very much for your review.

I replaced calloc with mbedtls_calloc inside of aligned_calloc, but do we sill need it for memory alignment?

@yanshay
Copy link
Contributor

yanshay commented May 23, 2025

I hope you don't mind me jumping in, but a small question I think is related here. Will the changes you make will allow utilizing PSRAM memory for esp-mbedtls?

Previous version afaik used the esp-wifi allocations and therefore the DRAM while technically I understand it could use PSRAM for at least some of the operations (unlike esp-wifi that must use DRAM).

@ivmarkov
Copy link
Collaborator

ivmarkov commented May 25, 2025

Thank you very much for your review.

I replaced calloc with mbedtls_calloc inside of aligned_calloc, but do we sill need it for memory alignment?

Great question that does not have a simple answer.

The TL;DR is: it should work without explicit alignment. This is so because malloc/calloc are required to return memory which is aligned to the biggest native C type supported in hardware that needs aligned RW. In other words, if e.g. u64 for your platform needs to be aligned at the 8-byte boundary (but e.g. u128 isn't), malloc for that platform is required to always return memory aligned to 8 bytes, regardless of how much memory you request it to allocate.

This works great for RiscV32 and Cortex M where malloc returns 4-bytes-aligned memory, and there only u32 needs to be aligned, bigger types don't have to.

For xtensa, it is a mess. While the xtensa spec does require aligned u64 (not sure for u128) and the LLVM xtensa rustc would even panic-complain (since recently, even in release!) if you try to dereference an un-aligned u64, esp-wifi's (or is it now esp-mallocs?) malloc emulation only alignes to 4 bytes, which is an omission I recently mentioned to @bjoernQ .

With that said, The C compiler (at least in release) does NOT complain on unaligned u64s and the CPU happily executes these unaligned u64 RWs just fine. Also, the ESP-IDF malloc impl also implements only 4-byte alignment and they say if you want higher alignment, you should use posix_memalign instead (probably they did it to conserve memory), but this way they violate the malloc contract.

Anyway, long story short, I say let's just use mbedtls_calloc (and thus potentially 4-bytes malloc) everywhere on xtensa too, unconditionally. As long as the C mbedtls structs do not contain u64s OR you do not dereference them from Rust, we should be fine. Should we see a panic in some of our own MbedTLS structures' allocations (to the others we don't have control) we either have to ask esp-wifi to align to 8 bytes, or we should implement our own little alignment routine on top of mbedtls_calloc.

@ivmarkov
Copy link
Collaborator

@AnthonyGrondin BTW if this is ready from your POV shall I merge?

@ivmarkov
Copy link
Collaborator

@AnthonyGrondin If you are interested in the xtensa alignment saga, check this thread: esp-rs/rust#195

... especially further down in the thread where I start pinging igrr from the Espressif team.

@AnthonyGrondin
Copy link
Collaborator Author

Code seems ready but we still need to do some testing.

I think the client examples broke and I need to figure out why.

@ivmarkov
Copy link
Collaborator

@AnthonyGrondin I think before thinking about connection splitting, we probably need to merge this one first...

@AnthonyGrondin
Copy link
Collaborator Author

I rebased on top of master and tested the examples on esp32s3.

mTLS client examples are still broken but they were broken on main so I don't think this is a bug that this PR introduces.

@AnthonyGrondin
Copy link
Collaborator Author

One last thing I'd like to see implemented here, is sharing certificates across different Certificates<'a> instances. Copying / Cloning the whole Certificates here is pointless because we pretty much pass it by reference everywhere, but there are cases where one would want to reuse the same CA chain for example, with another Certificates<'a> instance.

I have a case where I renew certificates on a device, so both certificates and private key are different, but the CA Chain is the same, and it would be a waste of memory to create and alloc another instance, if one exists already in memory.

I'm still trying to think how it could be implemented. Since we use alloc anyways, maybe some internal RC

@ivmarkov
Copy link
Collaborator

I'm still trying to think how it could be implemented. Since we use alloc anyways, maybe some internal RC

... except we actually no longer use alloc. Why not passing the CA chain as a regular reference into Certificates? This way you can share it trivially across.

(
BTW: All of this sharing can only happen within a single thread. (Not that this is important on bare-metal.). Just mentioning. The restriction is within the mbedtls C lib as well, I think, in that you can't share its structures across threads. At least not anymore.
)

@AnthonyGrondin
Copy link
Collaborator Author

AnthonyGrondin commented Jun 30, 2025

I'm still trying to think how it could be implemented. Since we use alloc anyways, maybe some internal RC

... except we actually no longer use alloc. Why not passing the CA chain as a regular reference into Certificates? This way you can share it trivially across.

( BTW: All of this sharing can only happen within a single thread. (Not that this is important on bare-metal.). Just mentioning. The restriction is within the mbedtls C lib as well, I think, in that you can't share its structures across threads. At least not anymore. )

I did a local refactor, removing the no_copy variants of Certificates and making with_certificates() / with_ca_chain() take &'d MbedTLSX509Crt<'_> (I did the same for PkContext) and store it within Certificates, so it basically becomes a facade. It works for simple examples such as the examples in this library, but for a more complex case such as within the app I'm developping, I get lifetime issues, because the underlying certificates MbedTLSX509Crt<'_> need to live for longer. It's harder to pass Certificates<'_> to different scopes.

Maybe I could derive Copy on Certificates and make Session take it owned so that the constructor looks something like the following, and Certificates is only a facade:

let mut session = Session::new(
    &mut socket,
    SessionConfig::new(
        Mode::Client {
            servername: SERVERNAME,
        },
        TlsVersion::Tls1_3,
    ),
    Certificates::new()
        .with_certificates(&crt, &pk)
        .with_ca_chain(&ca_chain),
    tls.reference(),
)

and let the user deal with the lifetime management of 3 different variables, instead of a single one.

EDIT: Or I could entirely remove the Certificates struct, make SessionConfig have:

    pub fn with_certificates(
        &mut self,
        certificate: &'d MbedTLSX509Crt<'_>,
        private_key: &'d PkContext,
    ) -> Self {
        self.certificate = Some(certificate);
        self.private_key = Some(private_key);
        self
    }
    
    pub fn with_ca_chain(&mut self, ca_chain: &'d MbedTLSX509Crt<'_>) -> Self {
        self.ca_chain = Some(ca_chain);
        self
    }

And then have:

let mut session = Session::new(
    &mut socket,
    SessionConfig::new(
        Mode::Client {
            servername: SERVERNAME,
        },
        TlsVersion::Tls1_3,
    )
    .with_certificates(&crt, &pk)
    .with_ca_chain(&ca_chain),
    tls.reference(),
)

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jun 30, 2025

I did a local refactor, removing the no_copy variants of Certificates and making with_certificates() / with_ca_chain() take &'d MbedTLSX509Crt<'> (I did the same for PkContext) and store it within Certificates, so it basically becomes a facade. It works for simple examples such as the examples in this library, but for a more complex case such as within the app I'm developping, I get lifetime issues, because the underlying certificates MbedTLSX509Crt<'> need to live for longer. It's harder to pass Certificates<'_> to different scopes.

Can you push your refactor somewhere? This sounds fishy. All lifetimes are covariant, so they should be possible to reduce to a single lifetime, unless I'm missing something.

@AnthonyGrondin
Copy link
Collaborator Author

I pushed the refactor to https://github.com/esp-rs/esp-mbedtls/tree/refactor/x509-memory-improvement-test

diff

I changed the edge_server example to reproduce something similar to what I'm doing in my application. It would work when Certificates owned the underlying certs, but now I would need to "Box::leak()" all the variants.

@ivmarkov
Copy link
Collaborator

I pushed the refactor to https://github.com/esp-rs/esp-mbedtls/tree/refactor/x509-memory-improvement-test

diff

I changed the edge_server example to reproduce something similar to what I'm doing in my application. It would work when Certificates owned the underlying certs, but now I would need to "Box::leak()" all the variants.

You mean this line:

let _: &'static Certificates = Box::leak(boxed);
?

Yes, it can't work, but I wonder how Box::new (previous line) actually even worked for your case? If I'm not mistaken (could be) Box requires where T: 'static and your Certificates are non-static as they hold references to the stack-allocated crt and private_key.

A few questions/comments:

  • Why do you want to Box Certificates in the first place? and why do you want 'static Certificates too?
  • If you want a 'static ref to Certificates (for whatever the reason is), then the refs inside Certificates must be static too. In other words, either you box-allocate crt and private_key and then leak them into static, or you just mk_static them prior to pushing their refs into Certificates. In either case you want Certificates<'static> so that when you leak it (or mk_static it) it would work and you'll get &'static Certificates<'static>

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jun 30, 2025

Update: No, Box<T> does not imply where T: 'static which is more flexible and why your Box::new works in the first place, but as I said above, unless T (= Certificates in your case) is indeed 'static (and in your case it isn't because of stack-allocated crt and priv_key to which you happen to keep a ref in your certificates), then Box::leak cannot return a 'static ref. Check the Box::leak docu for further reference.

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jun 30, 2025

Ah sorry, you probably said what I'm saying here:

but now I would need to "Box::leak()" all the variants

(if you mean crt and private_key)

Yes, that's unavoidable with this design. Alternative:
crt and private_key in Certificates become generified, i.e. Certificates<CRT, PK> with where CRT: Borrow<MbedTLSX509Crt>, PK: Borrow<PkContext>. Then we can have two type aliases: a pub type RefCertificates<'a> = Certificates<&'a MbedTLSX509Crt, &'a PkContext> and pub type OwnedCertificates = Certificates<MbedTLSX509Crt, PkContext>.

The point of this equilibristic is, if you want to have an easy life and use the allocator, with the thus-generified certificates you would also be able to Arc the crt and the private_key and push them as Arcs (or Rcs) too, and this way you share them in as many Certificates facades as you want, and you could still Box Certificates.

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jun 30, 2025

... so you could even roll-your-own type-alias: pub type RefCtCertificates = Certificates<Rc<MbedTLSX509Crt>, Rc<PkContext>>.

But the point is, esp-mbedtls does not know (or care) about you using alloc still.

@AnthonyGrondin
Copy link
Collaborator Author

AnthonyGrondin commented Jun 30, 2025

My use case is a bit unsafe to starts with. I wrap the HttpClient from https://github.com/drogue-iot/reqwless and I have a function to renew the certificates, on this wrapper, which takes &'_ mut HttpClient<'a, T, D> where I swap the TlsConfig to use my new certs, then make a call with my API to validate that they work and then:

  1. If failed, re-use the original certs, which stays unchanged
  2. Save the certs in flash storage, and update a certain static where they are stored.

I haven't found a safe easy way to do it but so far this seems to work for me. I only need to improve the memory usage.

I think in my final implementation I'll use a critical section or some blocking async to ensure that the client isn't used elsewhere during this certificate swap trickery. I could also create another client instance, but my current architecture uses a singleton for simplicity.

EDIT:

Initially, I was thinking about doing something like:

enum X509CrtInner {
    Owned(*mut mbedtls_x509_crt),
    Borrowed(*mut mbedtls_x509_crt),
}

struct MbedTLSX509Crt<'d> {
    crt: X509CrtInner,
    _t: PhantomData<&'d ()>,
}

impl Drop for MbedTLSX509Crt<'_> {
    fn drop(&mut self) {
        unsafe {
            match self.crt {
                X509CrtInner::Owned(ptr) => {
                    mbedtls_x509_crt_free(ptr);
                    mbedtls_free(ptr as *const _);
                }
                X509CrtInner::Borrowed(_) => {
                    // Do nothing
                }
            }
        }
    }
}

But managing the lifetime seemed to be an issue, and this is basically almost like reimplementing Rc

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jun 30, 2025

enum X509CrtInner {
    Owned(*mut mbedtls_x509_crt),
    Borrowed(*mut mbedtls_x509_crt),
}

Wouldn't a more correct variant of the above be something like:

enum X509CrtInner<'d> {
    Owned(*mut mbedtls_x509_crt),
    Borrowed(*mut mbedtls_x509_crt, PhantomData<&'d ()>),
}

A bit like the Cow<'d> type in Rust alloc?
The above can be made safe just fine, and the good thing is, if you have the Owned variant, X509CrtInner becomes X509CrtInner<'static>, just like the Cow::Owned enum variant in Rust alloc.

However if you want to "swap in" the cert, PhantomData<&'d ()> should be PhantomData<&'d mut ()> - that is - if the cert is not "owned" in the first place, and then changing the lifetime might have consequences, but I need to test to understand what these could be.

====

In any case, the only safe variant of what you are trying to achieve would be to fetch-renew-save-in-NVS the new certs in a completely separate loop/task, and then signal the HTTP client task to re-start itself. Meaning, bring down the HTTP client, re-load the (now new) certs from NVS into the static or whatever storage, and then start the client again?

Swapping the certs in the HTTP client when they are behind a & (immutable) reference - or even behind an Rc or Arc sounds a bit dangerous and maybe a UB to me. That is, without a proper interior mutability cell in there. like a mutex or a RefCell, which would be a bit of a pain to introduce in esp-mbedtls I think.

- Session now exposes an optional `auth_mode` parameter to select the
certificate verification mode.
- Update Certificates to handle no-copy variants of certificates
parsing.
- Add DER certificates for testing no-copy variants
- Remove duplicate justfile step for faster building
@AnthonyGrondin AnthonyGrondin force-pushed the feat/X509-memory-improvement branch from 9e87130 to d0d565a Compare July 7, 2025 20:06
@ivmarkov
Copy link
Collaborator

ivmarkov commented Jul 8, 2025

@AnthonyGrondin I'm not sure where we are landing with this in the end. Did you give up (for now) on sharing cert and private_key across multiple Certificates instances (either with a simple & or with a Borrow generic so that in the latter case the user could optionally also have the Certificate own these)?

@AnthonyGrondin
Copy link
Collaborator Author

@AnthonyGrondin I'm not sure where we are landing with this in the end. Did you give up (for now) on sharing cert and private_key across multiple Certificates instances (either with a simple & or with a Borrow generic so that in the latter case the user could optionally also have the Certificate own these)?

I've solved the issues of renewing certificates on my side, using a state machine.

As for sharing certificates between different instances, I did test both taking &'d MbedTLSX509Crt<'d> and doing something like:

enum X509CrtInner<'d> {
    Owned(*mut mbedtls_x509_crt),
    Borrowed(*mut mbedtls_x509_crt, PhantomData<&'d ()>),
}

Both ways I wasn't so quite happy with the API, but I can take a look back at it. In any ways, we would need to make MbedTLSX509Crt and PkContext public, which I wanted to avoid as they're just pointer wrappers, but this could simplify the API.

@ivmarkov
Copy link
Collaborator

ivmarkov commented Jul 8, 2025

There is still the Borrow option with generifying the Certificates struct. This means you can pass e.g. &PkContext, or just PkContext (as owned values implement Borrow of themselves too), or Rc<PkContext>. Of course both the MBedTlsX509Cert struct as well as the PkContext struct would then need to have both new constructor fn as well as drop fn to de-allocate their internal MbedTls pointers, once these structs run out of scope (and are no longer referenced anywhere).

Update: and would need to be pubic, yes.

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.

3 participants