Skip to content

Conversation

@facutuesca
Copy link
Contributor

API

This adds support for CHOICE fields using Python union type annotations.

@asn1.sequence
class Example:
    foo: int | bool | str  # CHOICE field

Since unions remove repeated/redundant types, we need a way to represent a CHOICE with multiple variants of the same type.

@asn1.sequence
class Example:
    foo: int | int  # does not work, gets simplified to `foo: int`

We do this by creating a trivial wrapper class, so that each variant is a different type:

@asn1.variant
class MyIntA:
    value: int

@asn1.variant
class MyIntB
    value: int

These can now be used in a union:

@asn1.sequence
class Example:
    foo: Annotated[MyIntA, asn1.Implicit(0)] | Annotated[MyIntB, asn1.Implicit(1)]

And then used to encode/decode:

obj = Example(foo=MyIntA(42))
encoded = encode_der(obj)

decoded = decode(Example, encoded)
assert isinstance(decoded.foo, MyIntA)
assert decoded.foo.value == 42

Details

  • Classes annotated with the @asn1.variant decorator can only have one field, it must be named value, and it should not have any annotations.
  • The @asn1.variant decorator adds a constructor so that the class can be initialized with just the value (e.g: MyInt(42))
  • The internal function type_to_tag() was replaced with expected_tags_for_type(), which does the same thing except it returns a list of tags. This is due to CHOICE types, where the expected tag can be one of many.

Part of #12283

Signed-off-by: Facundo Tuesca <facundo.tuesca@trailofbits.com>
@facutuesca facutuesca marked this pull request as ready for review January 22, 2026 04:05
@alex
Copy link
Member

alex commented Jan 24, 2026

I was going back through our notes, and we decided not to do asn1.Variant[int] because you wouldn't know which variant you selected, but is there a reason we can't do asn1.Varaint[int, "some tag"] and then you get an instanec of asn1.Variant[int](value, "some tag") or something? I guess I hate that, but I also hate every other API we've considered.

@facutuesca
Copy link
Contributor Author

facutuesca commented Jan 24, 2026

I was going back through our notes, and we decided not to do asn1.Variant[int] because you wouldn't know which variant you selected, but is there a reason we can't do asn1.Varaint[int, "some tag"] and then you get an instanec of asn1.Variant[int](value, "some tag") or something? I guess I hate that, but I also hate every other API we've considered.

Do you mean something like this?:

U = typing.TypeVar("U")
Tag = typing.TypeVar("Tag")

@dataclasses.dataclass(frozen=True)
class Variant(typing.Generic[U, Tag]):
    value: U
    tag: str


@dataclasses.dataclass(frozen=True)
class MyClass:
    foo: (
        Annotated[Variant[int, typing.Literal["Tag1"]], asn1.Implicit(0)]
        | Annotated[Variant[int, typing.Literal["Tag2"]], asn1.Implicit(1)]
        | Annotated[str, asn1.Implicit(2)]
        | Annotated[bool, asn1.Implicit(3)]
    )


# creating a value for encoding
a = MyClass(foo=Variant(3, "Tag1"))

# looking at a decoded value to see which variant it is
if isinstance(a.foo.value, int) and a.foo.tag == "Tag1":
    # Tag1 logic
elif isinstance(a.foo.value, int) and a.foo.tag == "Tag2":
    # Tag2 logic
elif isinstance(a.foo.value, str):
    # str logic
elif isinstance(a.foo.value, bool):
    # bool logic

@alex
Copy link
Member

alex commented Jan 24, 2026 via email

@facutuesca
Copy link
Contributor Author

Something like that, yeah.

PR open here: #14201

@facutuesca
Copy link
Contributor Author

closing in favor of #14201

@facutuesca facutuesca closed this Jan 29, 2026
@facutuesca facutuesca deleted the ft/asn1-choice branch January 29, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants