formats icon indicating copy to clipboard operation
formats copied to clipboard

der: Error stacktraces

Open dishmaker opened this issue 7 months ago • 4 comments

https://github.com/RustCrypto/formats/blob/bd840638835fcf66c4ffc3baed6d7c3b727c4ef3/der/src/decode.rs#L26-L28

I think associated Error would be useful inside Reader trait. [^1]

Motivation

To prove usefulness, suppose we want to return annotated errors with .field stacktrace.

Example of such stacktrace from invalid PKCS#15:

Error: from_der_type CIOChoice

Caused by:
   0: .choice AuthObjects
   0: .choice Objects
   0: [0]
   0: (AuthenticationObjectChoice)
   0: .choice Pwd
   0: .type_attributes
   0: .pwd_reference

   1: incorrect length for INTEGER at DER byte 57

Because some ASN.1 DER definitions are really complicated, such error stacktrace is very helpful. Field names in returned error are possible, because derive Sequence may generate .wrap_err_field("fieldname") calls.

Why not use Decode::Error?

Such errors may originate from x509-cert, as it is a part of PKCS#15:

CertificateChoice ::= CHOICE {
    x509Certificate CertificateObject {X509CertificateAttributes},
    -- ...
    spkiCertificate [1] CertificateObject {SPKICertificateAttributes},

Moreover, existing crates using der will benefit from such Decode error stacktrace feature, similar to my clarify Encode feature.

[^1]: I mentioned that some time ago https://github.com/RustCrypto/formats/issues/1053#issuecomment-2393628486

dishmaker avatar Jul 18 '25 08:07 dishmaker

As was mentioned in #1053, having an associated Error type on Decode/DecodeValue makes it possible to return format-specific errors. These can be well-typed e.g. enums which carry structured information about what went wrong, defined by downstream consumers of der in their own crates, as Decode/DecodeValue are actually traits impl'd by downstream consumers.

You are suggesting moving it to Reader, but Reader has an impl cardinality of 2 (SliceReader and PemReader) and both of those impls are in the der crate itself.

What you're describing doesn't sound like you actually want to leverage an associated error type? Why would it be different for SliceReader versus PemReader? Wouldn't they both use der::Error and at that point you don't need a separate associated type?

As an alternative to the well-typed errors an associated Error type enables, you are suggesting a stringly-typed field name (a 'static str I guess?) The two seem orthogonal to me: you can add a wrap_err_field method to der::Error and still keep the associated Error type. So it's a false dichotomy.

But a field name still provides far less information and flexibility than a proper format-specific error type. Enum variants can be used to relay more specific details about why processing of a particular field went wrong, e.g. perhaps it's trying to interpret an OCTET STRING as a particular cryptographic key, but the string is of the wrong length, or an OID identifies an unsupported algorithm. These are failures that can quite clearly be relayed using format-specific error types.

I'll also note format-specific error types have been requested by multiple people. See #1492 as one example.

tarcieri avatar Jul 18 '25 13:07 tarcieri

but Reader has an impl cardinality of 2 (SliceReader and PemReader)

What you're describing doesn't sound like you actually want to leverage an associated error type? Why would it be different for SliceReader versus PemReader?

I use my own wrapper of SliceReader with error stacktraces.

/// [`SliceReader`] which tracks struct names and field names, where the error occured.
#[derive(Clone, Debug)]
pub struct SliceDebugReader<'a> {
    reader: SliceReader<'a>,
}
impl<'a> Reader<'a> for SliceDebugReader<'a> {
    type Error = StackTracingError;
}

you can add a wrap_err_field method to der::Error and still keep the associated Error type.

It would carry Vec<String> and be suboptimal.

use thiserror::Error;


#[derive(Error, Debug)]
pub enum StackTracingError {
    #[error(transparent)]
    Io(#[from] std::io::Error),

    #[error("{stack}")]
    Der {
        stack: Vec<String>,

        #[source]
        der: der::DerError,
    },
}

dishmaker avatar Jul 18 '25 14:07 dishmaker

I use my own wrapper or SliceReader with error stacktraces.

That's not a justification for defining a custom error type on readers. It sounds like you just want more functionality out of the existing readers.

Are you expecting everyone to define their own bespoke SliceReader-like Reader types?

It would carry Vec<String> and be suboptimal.

Please be more specific. What solution are you suggesting that doesn't involve a Vec<String> to report on nested fields and why does it need a custom error type per reader?

(I had already suggested 'static str since fields names are static, as it were, but you would still need a Vec to report on a series of nested field names)

tarcieri avatar Jul 18 '25 14:07 tarcieri

Okay, I was too focused on a single solution and need to think that through.

dishmaker avatar Jul 18 '25 14:07 dishmaker

The ability to collect this information would be good. How about some struct ErrorContext or thereabouts, and a trait that Error types can impl to access it?

tarcieri avatar Feb 06 '26 21:02 tarcieri

der::Error trait with optional functions handling (field|struct|choice) names and (sequence|set) index would be great.

dishmaker avatar Feb 07 '26 14:02 dishmaker