formats icon indicating copy to clipboard operation
formats copied to clipboard

Support parsing X509 certificates with no allocations

Open DemiMarie opened this issue 1 year ago • 2 comments

WebPKI supports this, as does my own barebones-x509 crate.

DemiMarie avatar Nov 03 '24 17:11 DemiMarie

See ample related discussion in #689 for the encoding side of things.

We pretty much went in the opposite direction in #765 and leaned further into the existing hard dependency on alloc.

tarcieri avatar Nov 03 '24 17:11 tarcieri

In some other crates, notably spki and the latest prereleases of pkcs8, we do abstract over the owned and borrowed forms using generics:

https://docs.rs/pkcs8/0.11.0-rc.1/pkcs8/struct.PrivateKeyInfo.html

This has the following forms, one of which has a lifetime and the other does not:

  • https://docs.rs/pkcs8/0.11.0-rc.1/pkcs8/type.PrivateKeyInfoRef.html
  • https://docs.rs/pkcs8/0.11.0-rc.1/pkcs8/type.PrivateKeyInfoOwned.html

We could potentially do something like that with x509-cert as well, though we already use generics to carry a profile ZST, so it might become a lot of generic parameters, which makes the API more cumbersome.

tarcieri avatar Nov 04 '24 20:11 tarcieri

In benchmarks x509-cert Decode is the slowest (but does Encode very fast).

Image

Figure: x509-cert slowest (red), cryptography-x509 fastest

https://bencher.dev/perf/rasn/plots

Now there is opportunity to use alloc::Cow, as Tony refactored Ref types #1921

dishmaker avatar Jul 21 '25 09:07 dishmaker

Curious what that benchmark actually looks like. Other implementations may be deferring processing of things x509-cert processes eagerly.

I'd also be curious what the hotspots in x509-cert.

tarcieri avatar Jul 22 '25 02:07 tarcieri

Benchmark code: https://github.com/librasn/rasn/blob/0a65d9a5699037ec19a706ce1f776ecc87d94b90/benches/criterion.rs#L42-L46

pyca cryptography-x509: https://github.com/pyca/cryptography/blob/d20fcd0b439c6140772413aae589e3c617b39ac1/src/rust/cryptography-x509/src/certificate.rs#L40

dishmaker avatar Jul 22 '25 07:07 dishmaker

I saw other discussion on this topic, and I understand that the current design is crucial to making builder implementation simple and consistent.

What do you think about having a separate CertificateRef<'a> type, though, that only support parsing from existing DER bytes, and does not support building using a builder (or, really, any other way of creating instances)? On embedded devices, where this no_std, no_alloc scenario would be most useful, one rarely needs to create new certificates anyway, typically one only needs to verify a cert chain against trusted CA, and all of those are obtained by parsing DERs.

If someone posted a pull request with such feature, would you be willing to accept it? It would introduce new alloc feature, hide existing code that requires alloc behind it, and introduce new CertificateRef type that uses *Ref types from der crate where appropriate.

xyzzyz avatar Aug 01 '25 14:08 xyzzyz

Ideally we could avoid the duplication of a separate type and leverage generics instead, making CertificateRef a type alias, similar to how the pkcs8 crate works: https://docs.rs/pkcs8/0.11.0-rc.6/pkcs8/index.html#types

Though a separate type could be considered as a last resort. Since we already have a Profile generic and the family of types we'd use for storing the document is unrelated, it seems like we'd be talking about tacking on a second generic parameter to Certificate, which seems a bit complicated on its own.

tarcieri avatar Aug 01 '25 15:08 tarcieri

Having alloc and ref type aliases would have an obvious flaw of not supporting half-owned, half-ref certificates.

On the other hand, Cow cert would be useful for editing ref-parsed cert and replacing one field with some owned string, for example.

Personally, i use Cow ASN.1 DER for editing structures in egui in a browser under WASM.

Parsing and allocating 60 times a second, just to edit one Utf8String, would totally destroy GUI performance.

dishmaker avatar Aug 01 '25 16:08 dishmaker

@dishmaker we could consider making the allocating structure Cow-based. Generics would permit both a fully owned type as well as Cow based. But whatever we do there is orthogonal to the no-alloc use case.

tarcieri avatar Aug 01 '25 17:08 tarcieri

I think that leveraging generics is definitely viable. It would be hard to make this change in a backward-compatible way, though. x509_cert is still on 0.x.x version, so it would not be against SemVer, but obviously existing users might not be particularly excited about their code breaking on update.

How do you weigh this concern vs code duplication resulting from introducing separate CertificateRef type?

xyzzyz avatar Aug 02 '25 02:08 xyzzyz

We’re currently making breaking changes so that’s fine, but we will ideally start wrapping up soon

tarcieri avatar Aug 02 '25 02:08 tarcieri

@tarcieri would you (or some other RustCrypto developer/maintainer) be interested in contracted work for delivering this "X.509 certificates for no_std, no alloc environment" functionality? My organization is very interested in it.

xyzzyz avatar Aug 11 '25 12:08 xyzzyz

I don't have time to work on this, sorry, though there are other users who might potentially be interested.

tarcieri avatar Aug 11 '25 13:08 tarcieri

I'm afraid creating newtype OctetStringCow would be easier in x509-cert.

I tried implementing Encode/Decode for Cow here:

  • https://github.com/RustCrypto/formats/pull/2054

dishmaker avatar Nov 07 '25 15:11 dishmaker

@xyzzyz Which X.509 features do you need? I might be able to provide a very simple, purpose-built solution for your use-case. I have already written my own X.509 certificate parser (https://github.com/barebones-x509/barebones-x509) and while it is currently unmaintained I could fix that if someone was interested in funding the work.

DemiMarie avatar Nov 07 '25 20:11 DemiMarie

@dishmaker here's a PR that should make Decode/DecodeValue/ and Encode/EncodeValue as well as FixedTag work for Cow<OctetStringRef> (or any other type which impls the appropriate traits): https://github.com/RustCrypto/formats/pull/2093

As soon as you add a lifetime to the type though, it will break the blanket impl of DecodePem, which uses 1-pass PEM+DER decoding. Adding it back will require some way for the Reader to signal that it can't be borrowed from, and in that case the DecodeValue impl on Cow needs to decode the owned type, not the borrowed one.

tarcieri avatar Nov 07 '25 22:11 tarcieri