1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
4
5from __future__ import annotations
6
7import builtins
8import dataclasses
9import enum
10import sys
11import types
12import typing
13
14if sys.version_info < (3, 11):
15 import typing_extensions
16
17 LiteralString = typing_extensions.LiteralString
18else:
19 LiteralString = typing.LiteralString
20
21from cryptography.hazmat.bindings._rust import declarative_asn1
22from cryptography.hazmat.bindings._rust import x509 as rust_x509
23
24if sys.version_info < (3, 10):
25 NoneType = type(None)
26else:
27 NoneType = types.NoneType # type: ignore[nonetype-type]
28
29T = typing.TypeVar("T", covariant=True)
30U = typing.TypeVar("U")
31Tag = typing.TypeVar("Tag", bound=LiteralString)
32
33
34@dataclasses.dataclass(frozen=True)
35class Variant(typing.Generic[U, Tag]):
36 """
37 A tagged variant for CHOICE fields with the same underlying type.
38
39 Use this when you have multiple CHOICE alternatives with the same type
40 and need to distinguish between them:
41
42 foo: (
43 Annotated[Variant[int, typing.Literal["IntA"]], Implicit(0)]
44 | Annotated[Variant[int, typing.Literal["IntB"]], Implicit(1)]
45 )
46
47 Usage:
48 example = Example(foo=Variant(5, "IntA"))
49 decoded.foo.value # The int value
50 decoded.foo.tag # "IntA" or "IntB"
51 """
52
53 value: U
54 tag: str
55
56
57decode_der = declarative_asn1.decode_der
58encode_der = declarative_asn1.encode_der
59
60
61_X509_TYPES = (
62 rust_x509.Certificate,
63 rust_x509.CertificateSigningRequest,
64 rust_x509.CertificateRevocationList,
65)
66
67
68def _check_x509_field_annotations(
69 field_type: typing.Any,
70 annotation: declarative_asn1.Annotation,
71 field_name: str,
72) -> None:
73 if field_type in _X509_TYPES and isinstance(annotation.encoding, Implicit):
74 raise TypeError(
75 f"field '{field_name}' has an IMPLICIT annotation, but "
76 "IMPLICIT annotations are not supported for X.509 types."
77 )
78
79
80def _is_union(field_type: type) -> bool:
81 # NOTE: types.UnionType for `T | U`, typing.Union for `Union[T, U]`.
82 # TODO: Drop the `hasattr()` once the minimum supported Python version
83 # is >= 3.10.
84 union_types = (
85 (types.UnionType, typing.Union)
86 if hasattr(types, "UnionType")
87 else (typing.Union,)
88 )
89 return typing.get_origin(field_type) in union_types
90
91
92def _resolve_type_aliases(field_type: typing.Any) -> typing.Any:
93 # Recursively resolve PEP 695 (`type X = ...`) type aliases (Python
94 # 3.12+) to their underlying value, so that the rest of the
95 # normalization logic never encounters an alias. Aliases can refer
96 # to other aliases and can appear at any level of nesting (e.g.
97 # inside `Annotated[...]`, unions, or `list[...]`).
98 if sys.version_info < (3, 12):
99 return field_type
100
101 while isinstance(field_type, typing.TypeAliasType):
102 field_type = field_type.__value__
103
104 args = typing.get_args(field_type)
105 resolved_args = tuple(_resolve_type_aliases(arg) for arg in args)
106 if resolved_args == args:
107 # No aliases anywhere inside: return the type unchanged.
108 return field_type
109
110 if _is_union(field_type):
111 # `X | Y` unions can't be rebuilt through their origin like
112 # other generics below: `typing.get_origin` returns
113 # `types.UnionType` for them, which is only subscriptable on
114 # Python 3.14+. Rebuilding through `typing.Union` also
115 # flattens any nested union introduced by an alias of a union
116 # (e.g. `Time | int` where `type Time = UTCTime |
117 # GeneralizedTime`), just like `typing.Union` would have done
118 # if the alias had been written inline.
119 return typing.Union[resolved_args]
120
121 # An alias appeared inside a generic (e.g. `Annotated[Time, ...]`,
122 # `list[MyInt]`, or `SetOf[MyInt]`): re-parameterize the generic
123 # with the resolved arguments. Subscripting with a tuple is
124 # equivalent to subscripting with multiple arguments.
125 return typing.get_origin(field_type)[resolved_args]
126
127
128def _extract_annotation(
129 metadata: tuple, field_name: str
130) -> declarative_asn1.Annotation:
131 default = None
132 encoding = None
133 size = None
134 for raw_annotation in metadata:
135 if isinstance(raw_annotation, Default):
136 if default is not None:
137 raise TypeError(
138 f"multiple DEFAULT annotations found in field "
139 f"'{field_name}'"
140 )
141 default = raw_annotation.value
142 elif isinstance(raw_annotation, declarative_asn1.Encoding):
143 if encoding is not None:
144 raise TypeError(
145 f"multiple IMPLICIT/EXPLICIT annotations found in field "
146 f"'{field_name}'"
147 )
148 encoding = raw_annotation
149 elif isinstance(raw_annotation, declarative_asn1.Size):
150 if size is not None:
151 raise TypeError(
152 f"multiple SIZE annotations found in field '{field_name}'"
153 )
154 size = raw_annotation
155 else:
156 raise TypeError(f"unsupported annotation: {raw_annotation}")
157
158 return declarative_asn1.Annotation(
159 default=default, encoding=encoding, size=size
160 )
161
162
163def _normalize_field_type(
164 field_type: typing.Any, field_name: str
165) -> declarative_asn1.AnnotatedType:
166 field_type = _resolve_type_aliases(field_type)
167
168 # Strip the `Annotated[...]` off, and populate the annotation
169 # from it if it exists.
170 if typing.get_origin(field_type) is typing.Annotated:
171 annotation = _extract_annotation(field_type.__metadata__, field_name)
172 field_type, *_ = typing.get_args(field_type)
173 else:
174 annotation = declarative_asn1.Annotation()
175
176 if annotation.size is not None and (
177 typing.get_origin(field_type) not in (builtins.list, SetOf)
178 and field_type
179 not in (
180 builtins.bytes,
181 builtins.str,
182 BitString,
183 IA5String,
184 PrintableString,
185 )
186 ):
187 raise TypeError(
188 f"field '{field_name}' has a SIZE annotation, but SIZE "
189 "annotations are only supported for fields of types: "
190 "[SEQUENCE OF, SET OF, BIT STRING, OCTET STRING, UTF8String, "
191 "PrintableString, IA5String]"
192 )
193
194 if field_type is TLV:
195 if isinstance(annotation.encoding, Implicit):
196 raise TypeError(
197 f"field '{field_name}' has an IMPLICIT annotation, but "
198 "IMPLICIT annotations are not supported for TLV types."
199 )
200 elif annotation.default is not None:
201 raise TypeError(
202 f"field '{field_name}' has a DEFAULT annotation, but "
203 "DEFAULT annotations are not supported for TLV types."
204 )
205
206 _check_x509_field_annotations(field_type, annotation, field_name)
207
208 if hasattr(field_type, "__asn1_root__"):
209 root_type = field_type.__asn1_root__
210 if not isinstance(
211 root_type,
212 (
213 declarative_asn1.Type.Sequence,
214 declarative_asn1.Type.Set,
215 declarative_asn1.Type.ValueSet,
216 ),
217 ):
218 raise TypeError(f"unsupported root type: {root_type}")
219 return declarative_asn1.AnnotatedType(
220 typing.cast(declarative_asn1.Type, root_type), annotation
221 )
222 elif _is_union(field_type):
223 union_args = typing.get_args(field_type)
224 if len(union_args) == 2 and NoneType in union_args:
225 # A Union between a type and None is an OPTIONAL
226 optional_type = (
227 union_args[0] if union_args[1] is type(None) else union_args[1]
228 )
229 if optional_type is TLV:
230 raise TypeError(
231 "optional TLV types (`TLV | None`) are not "
232 "currently supported"
233 )
234 # For optional types, the annotation is associated with the
235 # union, so we check it against the inner type here.
236 _check_x509_field_annotations(
237 optional_type, annotation, field_name
238 )
239 annotated_type = _normalize_field_type(optional_type, field_name)
240
241 if not annotated_type.annotation.is_empty():
242 raise TypeError(
243 "optional (`X | None`) types cannot have `X` "
244 "annotated: annotations must apply to the union "
245 "(i.e: `Annotated[X | None, annotation]`)"
246 )
247
248 if annotation.default is not None:
249 raise TypeError(
250 "optional (`X | None`) types should not have a DEFAULT "
251 "annotation"
252 )
253
254 rust_field_type = declarative_asn1.Type.Option(annotated_type)
255
256 else:
257 # Otherwise, the Union is a CHOICE
258 if isinstance(annotation.encoding, Implicit):
259 # CHOICEs cannot be IMPLICIT. See X.680 section 31.2.9.
260 raise TypeError(
261 "CHOICE (`X | Y | ...`) types should not have an IMPLICIT "
262 "annotation"
263 )
264 variants = [
265 _type_to_variant(arg, field_name)
266 for arg in union_args
267 if arg is not type(None)
268 ]
269
270 # Union types should either be all Variants
271 # (`Variant[..] | Variant[..] | etc`) or all non Variants
272 are_union_types_tagged = variants[0].tag_name is not None
273 if any(
274 (v.tag_name is not None) != are_union_types_tagged
275 for v in variants
276 ):
277 raise TypeError(
278 "When using `asn1.Variant` in a union, all the other "
279 "types in the union must also be `asn1.Variant`"
280 )
281
282 if are_union_types_tagged:
283 tags = {v.tag_name for v in variants}
284 if len(variants) != len(tags):
285 raise TypeError(
286 "When using `asn1.Variant` in a union, the tags used "
287 "must be unique"
288 )
289
290 rust_choice_type = declarative_asn1.Type.Choice(variants)
291 # If None is part of the union types, this is an OPTIONAL CHOICE
292 rust_field_type = (
293 declarative_asn1.Type.Option(
294 declarative_asn1.AnnotatedType(
295 rust_choice_type, declarative_asn1.Annotation()
296 )
297 )
298 if NoneType in union_args
299 else rust_choice_type
300 )
301
302 elif typing.get_origin(field_type) is builtins.list:
303 inner_type = _normalize_field_type(
304 typing.get_args(field_type)[0], field_name
305 )
306 rust_field_type = declarative_asn1.Type.SequenceOf(inner_type)
307 elif typing.get_origin(field_type) is SetOf:
308 inner_type = _normalize_field_type(
309 typing.get_args(field_type)[0], field_name
310 )
311 rust_field_type = declarative_asn1.Type.SetOf(inner_type)
312 else:
313 rust_field_type = declarative_asn1.non_root_python_to_rust(field_type)
314
315 return declarative_asn1.AnnotatedType(rust_field_type, annotation)
316
317
318# Convert a type to a Variant. Used with types inside Union
319# annotations (T1, T2, etc in `Union[T1, T2, ...]`).
320def _type_to_variant(
321 t: typing.Any, field_name: str
322) -> declarative_asn1.Variant:
323 is_annotated = typing.get_origin(t) is typing.Annotated
324 inner_type = typing.get_args(t)[0] if is_annotated else t
325
326 # Check if this is a Variant[T, Tag] type
327 if typing.get_origin(inner_type) is Variant:
328 value_type, tag_literal = typing.get_args(inner_type)
329 if typing.get_origin(tag_literal) is not typing.Literal:
330 raise TypeError(
331 "When using `asn1.Variant` in a type annotation, the second "
332 "type parameter must be a `typing.Literal` type. E.g: "
333 '`Variant[int, typing.Literal["MyInt"]]`.'
334 )
335 tag_name = typing.get_args(tag_literal)[0]
336
337 if hasattr(value_type, "__asn1_root__"):
338 rust_type = value_type.__asn1_root__
339 else:
340 rust_type = declarative_asn1.non_root_python_to_rust(value_type)
341
342 if is_annotated:
343 ann_type = declarative_asn1.AnnotatedType(
344 rust_type,
345 _extract_annotation(t.__metadata__, field_name),
346 )
347 else:
348 ann_type = declarative_asn1.AnnotatedType(
349 rust_type,
350 declarative_asn1.Annotation(),
351 )
352
353 return declarative_asn1.Variant(Variant, ann_type, tag_name)
354 else:
355 # Plain type (not a tagged Variant)
356 return declarative_asn1.Variant(
357 inner_type,
358 _normalize_field_type(t, field_name),
359 None,
360 )
361
362
363def _annotate_fields(
364 raw_fields: dict[str, type],
365) -> dict[str, declarative_asn1.AnnotatedType]:
366 fields = {}
367 for field_name, field_type in raw_fields.items():
368 # Recursively normalize the field type into something that the
369 # Rust code can understand.
370 annotated_field_type = _normalize_field_type(field_type, field_name)
371 fields[field_name] = annotated_field_type
372
373 return fields
374
375
376def _register_asn1_sequence(cls: type[U]) -> None:
377 raw_fields = typing.get_type_hints(cls, include_extras=True)
378 root = declarative_asn1.Type.Sequence(cls, _annotate_fields(raw_fields))
379
380 setattr(cls, "__asn1_root__", root)
381
382
383def _register_asn1_set(cls: type[U]) -> None:
384 raw_fields = typing.get_type_hints(cls, include_extras=True)
385 root = declarative_asn1.Type.Set(cls, _annotate_fields(raw_fields))
386
387 setattr(cls, "__asn1_root__", root)
388
389
390# Due to https://github.com/python/mypy/issues/19731, we can't define an alias
391# for `dataclass_transform` that conditionally points to `typing` or
392# `typing_extensions` depending on the Python version. We work around it by
393# making the whole decorated class conditional on the Python version.
394if sys.version_info < (3, 11):
395
396 @typing_extensions.dataclass_transform(kw_only_default=True)
397 def sequence(cls: type[U]) -> type[U]:
398 # We use `dataclasses.dataclass` to add an __init__ method
399 # to the class with keyword-only parameters.
400 if sys.version_info >= (3, 10):
401 dataclass_cls = dataclasses.dataclass(
402 repr=False,
403 eq=False,
404 # `match_args` was added in Python 3.10 and defaults
405 # to True
406 match_args=False,
407 # `kw_only` was added in Python 3.10 and defaults to
408 # False
409 kw_only=True,
410 )(cls)
411 else:
412 dataclass_cls = dataclasses.dataclass(
413 repr=False,
414 eq=False,
415 )(cls)
416 _register_asn1_sequence(dataclass_cls)
417 return dataclass_cls
418
419 @typing_extensions.dataclass_transform(kw_only_default=True)
420 def set(cls: type[U]) -> type[U]:
421 # We use `dataclasses.dataclass` to add an __init__ method
422 # to the class with keyword-only parameters.
423 if sys.version_info >= (3, 10):
424 dataclass_cls = dataclasses.dataclass(
425 repr=False,
426 eq=False,
427 # `match_args` was added in Python 3.10 and defaults
428 # to True
429 match_args=False,
430 # `kw_only` was added in Python 3.10 and defaults to
431 # False
432 kw_only=True,
433 )(cls)
434 else:
435 dataclass_cls = dataclasses.dataclass(
436 repr=False,
437 eq=False,
438 )(cls)
439 _register_asn1_set(dataclass_cls)
440 return dataclass_cls
441
442else:
443
444 @typing.dataclass_transform(kw_only_default=True)
445 def sequence(cls: type[U]) -> type[U]:
446 # Only add an __init__ method, with keyword-only
447 # parameters.
448 dataclass_cls = dataclasses.dataclass(
449 repr=False,
450 eq=False,
451 match_args=False,
452 kw_only=True,
453 )(cls)
454 _register_asn1_sequence(dataclass_cls)
455 return dataclass_cls
456
457 @typing.dataclass_transform(kw_only_default=True)
458 def set(cls: type[U]) -> type[U]:
459 # Only add an __init__ method, with keyword-only
460 # parameters.
461 dataclass_cls = dataclasses.dataclass(
462 repr=False,
463 eq=False,
464 match_args=False,
465 kw_only=True,
466 )(cls)
467 _register_asn1_set(dataclass_cls)
468 return dataclass_cls
469
470
471def value_set(
472 value_type: type,
473) -> typing.Callable[[type[U]], type[U]]:
474 """
475 A class decorator that registers an `enum.Enum` subclass as an
476 ASN.1 value set of the given underlying type. All the member
477 values must be instances of `value_type`. Members are encoded as
478 their value; decoding fails if the decoded value does not match
479 any member.
480 """
481 rust_type = declarative_asn1.non_root_python_to_rust(value_type)
482
483 def decorator(cls: type[U]) -> type[U]:
484 if not issubclass(cls, enum.Enum):
485 raise TypeError(
486 "value sets can only be defined from enum.Enum subclasses"
487 )
488 members = list(cls)
489 if not members:
490 raise TypeError(
491 f"value set '{cls.__name__}' must have at least one member"
492 )
493 for member in members:
494 if not isinstance(member.value, value_type):
495 raise TypeError(
496 f"member '{member.name}' of value set '{cls.__name__}' "
497 f"must have a value of type "
498 f"'{value_type.__name__}', got: "
499 f"'{type(member.value).__name__}'"
500 )
501 inner = declarative_asn1.AnnotatedType(
502 rust_type, declarative_asn1.Annotation()
503 )
504 # Map from member value to member, used for O(1) lookups when
505 # decoding. This requires the member values to be hashable.
506 value_map = {member.value: member for member in members}
507 root = declarative_asn1.Type.ValueSet(cls, inner, value_map)
508
509 setattr(cls, "__asn1_root__", root)
510 return cls
511
512 return decorator
513
514
515# TODO: replace with `Default[U]` once the min Python version is >= 3.12
516@dataclasses.dataclass(frozen=True)
517class Default(typing.Generic[U]):
518 value: U
519
520
521SetOf = declarative_asn1.SetOf
522
523Explicit = declarative_asn1.Encoding.Explicit
524Implicit = declarative_asn1.Encoding.Implicit
525Size = declarative_asn1.Size
526
527PrintableString = declarative_asn1.PrintableString
528IA5String = declarative_asn1.IA5String
529UTCTime = declarative_asn1.UTCTime
530GeneralizedTime = declarative_asn1.GeneralizedTime
531BitString = declarative_asn1.BitString
532TLV = declarative_asn1.Tlv
533Null = declarative_asn1.Null