RustCrypto / formats

Cryptography-related format encoders/decoders: DER, PEM, PKCS, PKIX
228 stars 122 forks source link

[question] how to handle application tags that preceed other values #1385

Open Firstyear opened 2 months ago

Firstyear commented 2 months ago

I'm currently in the process of writing a kerberos encoder/decoder using this DER crate. In https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1 the specification is:

AS-REQ          ::= [APPLICATION 10] KDC-REQ

TGS-REQ         ::= [APPLICATION 12] KDC-REQ

KDC-REQ         ::= SEQUENCE {
        -- NOTE: first tag is [1], not [0]
        pvno            [1] INTEGER (5) ,
        msg-type        [2] INTEGER (10 -- AS -- | 12 -- TGS --),
        padata          [3] SEQUENCE OF PA-DATA OPTIONAL
                            -- NOTE: not empty --,
        req-body        [4] KDC-REQ-BODY
}

For an example: https://asn1.jsteel.dev/#aoGyMIGvoQMCAQWiAwIBCqMaMBgwCqEEAgIAlqICBAAwCqEEAgIAlaICBACkgYYwgYOgBwMFAAAAABChFDASoAMCAQGhCzAJGwd3aWxsaWFtogsbCUtLRENQLkRFVqMeMBygAwIBAqEVMBMbBmtyYnRndBsJS0tEQ1AuREVWpREYDzIwMjQwNDE3MDQxNTQ5WqcGAgR_vaeuqBowGAIBEgIBEQIBFAIBEwIBEAIBFwIBGQIBGg

The problem I'm having is I can't see how I would handle this with der_derive. I can't map "application tags" to an enum of potential inner variants.

And from the "reader" types if I implemented it manually, I see there is https://docs.rs/der/0.7.9/der/trait.Reader.html#method.peek_tag but not "pop_tag". I probably need to try and experiment a bit more, but do you have any advice on how to decode this "nicely"?

PS: Would this crate be opposed to some extra additions that have some kerberos or LDAP specific use cases? For example, Kerberos needs an IA5String that has a GeneralString Tag so that would be a KerberosString type.

Thanks!

tarcieri commented 2 months ago

If you're running into an limitation of custom derive you may just need to write the trait impls manually. Several crates in this repo do that as well to avoid a hard dependency on a custom derive stack, see e.g. pkcs8.

Since we don't have first-class support for fields with application tags, you'll just need to decode the Tag, introspect it, and then determine how to decode the value, similar to how context-specific tags are handled.

tarcieri commented 2 months ago

For example, Kerberos needs an IA5String that has a GeneralString Tag so that would be a KerberosString type.

It seems like you should be able to define your own Ia5String newtype for this, which decodes the tag, checks that it's GeneralString, and then uses the DecodeValue impl on Ia5String.

Firstyear commented 2 months ago

For example, Kerberos needs an IA5String that has a GeneralString Tag so that would be a KerberosString type.

It seems like you should be able to define your own Ia5String newtype for this, which decodes the tag, checks that it's GeneralString, and then uses the DecodeValue impl on Ia5String.

The problem here is some of your (very nice :) ) types like StrOwned and StrRef are private, and not being able to modify "Tag" to contain GeneralString as an option makes derived types not possible.

Which probably means at some point I have to give up and manual implement all my Decode's, but I was hoping to use the Derive macros :)

This is why I thought to ask about including these to this library, since it pretty easy to use/maintain KerberosString and KerberosTime since they are effectively copy-pastes of Ia5String and GeneralisedTime with minor changes (tag is GeneralString on KString and time can't have fractional components on KTime.).

The alternative is I externally re-invent both, which Im happy to do if you don't want to include these. But I thought I'd ask :)

EDIT: Would it be possible to expose some of your inner types like StrOwned and StrRef for users?

tarcieri commented 2 months ago

Instead of copy/pasting Ia5String, you can make a KerberosString newtype like struct KerberosString(Ia5String).

You will have to use handwritten trait impls, but that's what we'd do for a custom string type in der anyway. It's pretty much unavoidable.

The tag is just:

impl FixedTag for KerberosString {
    const TAG: Tag = Tag::GeneralString;
}

You can forward the DecodeValue/EncodeValue impls to the inner Ia5String, and that's pretty much all you need.

EDIT: Would it be possible to expose some of your inner types like StrOwned and StrRef for users?

They're deliberately unexposed so we can make breaking changes to them easily, although perhaps they're at a point we can consider exposing them.

Firstyear commented 2 months ago

Yep, I'll do some more toying about for a bit. Thanks for your advice mate, it's really appreciated.

Firstyear commented 2 months ago

Yeah, I'm stuck. I don't actually know how I would proceed to decode that structure. I can't get context specific tagging to work on the pvno value once I have consumed the application tag from the sequence. I would really appreciate any advice you have on how to structure this, because I've been reading the pkcs8 and source code and can't work out how the pieces are meant to fit together. Any advice or support would be wonderful on how to use this better.

AS-REQ          ::= [APPLICATION 10] KDC-REQ

KDC-REQ         ::= SEQUENCE {
        -- NOTE: first tag is [1], not [0]
        pvno            [1] INTEGER (5) ,
        msg-type        [2] INTEGER (10 -- AS -- | 12 -- TGS --),
        padata          [3] SEQUENCE OF PA-DATA OPTIONAL
                            -- NOTE: not empty --,
        req-body        [4] KDC-REQ-BODY
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KdcReq {
    pub pvno: Int,
}

impl<'a> ::der::Decode<'a> for KdcReq {
    fn decode<R: ::der::Reader<'a>>(reader: &mut R) -> ::der::Result<Self> {
        // =================
        // This fails with: Unable to decode as_req: Error { kind: Length { tag: Tag(0x81: CONTEXT-SPECIFIC [1] (primitive)) }, position: Some(Length(3)) }
        let pvno = reader
            .context_specific(der::TagNumber::new(1), der::TagMode::Explicit)?
            .ok_or_else(|| {
                der::Error::new(der::ErrorKind::Failed, der::Length::ZERO )
            })?;

        Ok(KdcReq {
            pvno
        })
    }
}

pub enum KerbMessage {
    AsReq(KdcReq),
}

impl<'a> ::der::Decode<'a> for KerbMessage {
    fn decode<R: ::der::Reader<'a>>(reader: &mut R) -> ::der::Result<Self> {
        let tag_as_req: der::Tag = der::TagNumber::N10.application(true);

        // let tag: der::Tag = reader.peek_tag()?;
        let tag: der::Tag = reader.decode()?;

        assert_eq!(tag, tag_as_req);

        match tag {
            tag_as_req => {
                let kdc_req: KdcReq = reader
                    .decode()?;
                Ok(KerbMessage::AsReq(kdc_req))
            }
            _ => unimplemented!(),
        }
    }
}
tarcieri commented 2 months ago

Can you give me a minimal example in hex along with the minimal schema you're having trouble decoding?

Firstyear commented 2 months ago

https://asn1.jsteel.dev/#aoGyMIGvoQMCAQWiAwIBCqMaMBgwCqEEAgIAlqICBAAwCqEEAgIAlaICBACkgYYwgYOgBwMFAAAAABChFDASoAMCAQGhCzAJGwd3aWxsaWFtogsbCUtLRENQLkRFVqMeMBygAwIBAqEVMBMbBmtyYnRndBsJS0tEQ1AuREVWpREYDzIwMjQwNDE3MDQxNTQ5WqcGAgR_vaeuqBowGAIBEgIBEQIBFAIBEwIBEAIBFwIBGQIBGg

Base64 URL Safe: aoGyMIGvoQMCAQWiAwIBCqMaMBgwCqEEAgIAlqICBAAwCqEEAgIAlaICBACkgYYwgYOgBwMFAAAAABChFDASoAMCAQGhCzAJGwd3aWxsaWFtogsbCUtLRENQLkRFVqMeMBygAwIBAqEVMBMbBmtyYnRndBsJS0tEQ1AuREVWpREYDzIwMjQwNDE3M DQxNTQ5WqcGAgR_vaeuqBowGAIBEgIBEQIBFAIBEwIBEAIBFwIBGQIBGg

It's the smallest example I have here, but should be the "problem structure" :)

tarcieri commented 2 months ago

From what I can tell, your code example is failing because you're not decoding the outer SEQUENCE tag first in the Decode impl on KdcReq. That said...

You should be able to use Sequence custom derive for KdcReq, using the context_specific attribute for each of the fields: https://docs.rs/der_derive/latest/der_derive/#asn1context_specific---attribute-context-specific-support

Here's an example, though it appears you'd want it to be EXPLICIT: https://github.com/RustCrypto/formats/blob/8ae3e29e16bcbecd78dc08041746479414db7f45/x509-tsp/src/lib.rs#L135-L138

You only need the handwritten Decode impl on KerbMessage, which it may make sense to model as a Choice.