Skip to content

Commit 887ff3f

Browse files
authored
asn1: Add tutorial to the docs (#14624)
* asn1: Update CHANGELOG * asn1: Add tutorial doc * add 'declaratively' to spelling wordlist
1 parent 8849682 commit 887ff3f

File tree

5 files changed

+348
-1
lines changed

5 files changed

+348
-1
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Changelog
9191
hybrid authenticated encryption.
9292
* Added new :doc:`/hazmat/primitives/asymmetric/mldsa` module with
9393
support for ML-DSA signing and verification with the AWS-LC backend.
94+
* Added new :doc:`/hazmat/asn1/index` module with support for declaratively
95+
defining custom ASN.1 types and encoding/decoding them.
9496

9597
.. v46-0-7:
9698

docs/hazmat/asn1/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ ASN.1
1010
.. toctree::
1111
:maxdepth: 2
1212

13+
tutorial
1314
reference

docs/hazmat/asn1/reference.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
ASN.1 Reference
44
===============
55

6-
.. currentmodule:: cryptography.hazmat.asn1
6+
.. module:: cryptography.hazmat.asn1
77

88
This module provides a declarative interface for defining ASN.1 structures
99
and serializing/deserializing them to/from DER-encoded data.

docs/hazmat/asn1/tutorial.rst

Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
.. hazmat::
2+
3+
Tutorial
4+
========
5+
6+
.. note::
7+
While usable, these APIs should be considered unstable and not yet
8+
subject to our backwards compatibility policy.
9+
10+
The :mod:`cryptography.hazmat.asn1` module provides a declarative API for
11+
working with ASN.1 data. ASN.1 structures are defined as Python classes
12+
with type annotations, and the module uses those definitions to
13+
serialize and deserialize instances to and from DER-encoded bytes.
14+
15+
Type mapping
16+
------------
17+
18+
The following table shows how ASN.1 types map to Python types:
19+
20+
.. list-table::
21+
:header-rows: 1
22+
23+
* - ASN.1 type
24+
- Python type
25+
* - ``BOOLEAN``
26+
- :class:`bool`
27+
* - ``INTEGER``
28+
- :class:`int`
29+
* - ``BIT STRING``
30+
- :class:`~cryptography.hazmat.asn1.BitString`
31+
* - ``OCTET STRING``
32+
- :class:`bytes`
33+
* - ``NULL``
34+
- :class:`~cryptography.hazmat.asn1.Null`
35+
* - ``OBJECT IDENTIFIER``
36+
- :class:`~cryptography.x509.ObjectIdentifier`
37+
* - ``UTF8String``
38+
- :class:`str`
39+
* - ``PrintableString``
40+
- :class:`~cryptography.hazmat.asn1.PrintableString`
41+
* - ``IA5String``
42+
- :class:`~cryptography.hazmat.asn1.IA5String`
43+
* - ``UTCTime``
44+
- :class:`~cryptography.hazmat.asn1.UTCTime`
45+
* - ``GeneralizedTime``
46+
- :class:`~cryptography.hazmat.asn1.GeneralizedTime`
47+
* - ``SEQUENCE``
48+
- :func:`@sequence <cryptography.hazmat.asn1.sequence>`-decorated class
49+
* - ``SEQUENCE OF``
50+
- :class:`list`\[T]
51+
* - ``SET OF``
52+
- :class:`~cryptography.hazmat.asn1.SetOf`\[T]
53+
* - ``CHOICE``
54+
- ``X | Y | ...``
55+
* - ``ANY``
56+
- :class:`~cryptography.hazmat.asn1.TLV`
57+
* - ``OPTIONAL``
58+
- ``X | None``
59+
60+
Defining a SEQUENCE
61+
-------------------
62+
63+
ASN.1 ``SEQUENCE`` types map to Python classes decorated with
64+
:func:`@sequence <cryptography.hazmat.asn1.sequence>`. Fields are defined as type annotations. For example,
65+
given the following ASN.1 definition:
66+
67+
.. code-block:: none
68+
69+
Point ::= SEQUENCE {
70+
x INTEGER,
71+
y INTEGER }
72+
73+
The corresponding Python definition is:
74+
75+
.. doctest::
76+
77+
>>> from cryptography.hazmat import asn1
78+
>>> @asn1.sequence
79+
... class Point:
80+
... x: int
81+
... y: int
82+
83+
The decorator adds an ``__init__`` with keyword-only parameters:
84+
85+
.. doctest::
86+
87+
>>> p = Point(x=3, y=7)
88+
>>> p.x
89+
3
90+
91+
Encoding and decoding
92+
---------------------
93+
94+
Use :func:`~cryptography.hazmat.asn1.encode_der` to serialize an ASN.1 object to DER bytes, and
95+
:func:`~cryptography.hazmat.asn1.decode_der` to deserialize:
96+
97+
.. doctest::
98+
99+
>>> from cryptography.hazmat import asn1
100+
>>> @asn1.sequence
101+
... class Point:
102+
... x: int
103+
... y: int
104+
>>> encoded = asn1.encode_der(Point(x=1, y=2))
105+
>>> encoded
106+
b'0\x06\x02\x01\x01\x02\x01\x02'
107+
>>> point = asn1.decode_der(Point, encoded)
108+
>>> point.x
109+
1
110+
>>> point.y
111+
2
112+
113+
Primitive types can also be encoded and decoded directly, without wrapping
114+
them in a sequence:
115+
116+
.. doctest::
117+
118+
>>> asn1.encode_der(42)
119+
b'\x02\x01*'
120+
>>> asn1.decode_der(int, b'\x02\x01*')
121+
42
122+
123+
Nested sequences
124+
----------------
125+
126+
Sequences can contain other sequences as field types:
127+
128+
.. doctest::
129+
130+
>>> from cryptography.hazmat import asn1
131+
>>> @asn1.sequence
132+
... class Name:
133+
... value: str
134+
>>> @asn1.sequence
135+
... class Certificate:
136+
... version: int
137+
... subject: Name
138+
>>> cert = Certificate(version=1, subject=Name(value="Alice"))
139+
>>> decoded = asn1.decode_der(Certificate, asn1.encode_der(cert))
140+
>>> decoded.subject.value
141+
'Alice'
142+
143+
OPTIONAL fields
144+
---------------
145+
146+
A field with a ``Union[X, None]`` (or ``X | None``) type annotation is
147+
treated as ASN.1 ``OPTIONAL``. When the value is ``None``, the field is
148+
omitted from the encoding:
149+
150+
.. doctest::
151+
152+
>>> import typing
153+
>>> from cryptography.hazmat import asn1
154+
>>> @asn1.sequence
155+
... class Record:
156+
... required: int
157+
... optional: typing.Union[str, None]
158+
>>> asn1.encode_der(Record(required=1, optional="hi"))
159+
b'0\x07\x02\x01\x01\x0c\x02hi'
160+
>>> asn1.encode_der(Record(required=1, optional=None))
161+
b'0\x03\x02\x01\x01'
162+
163+
DEFAULT values
164+
--------------
165+
166+
Use :class:`~cryptography.hazmat.asn1.Default` with :data:`typing.Annotated` to specify a default
167+
value for a field. When encoding, if the field's value equals the default,
168+
it is omitted. When decoding, if the field is absent, the default is used:
169+
170+
.. doctest::
171+
172+
>>> from typing import Annotated
173+
>>> from cryptography.hazmat import asn1
174+
>>> @asn1.sequence
175+
... class VersionedRecord:
176+
... version: Annotated[int, asn1.Default(0)]
177+
... data: bytes
178+
>>> asn1.encode_der(VersionedRecord(version=1, data=b"\x01"))
179+
b'0\x06\x02\x01\x01\x04\x01\x01'
180+
>>> # version=0 equals the default, so it is omitted from the encoding
181+
>>> asn1.encode_der(VersionedRecord(version=0, data=b"\x01"))
182+
b'0\x03\x04\x01\x01'
183+
184+
CHOICE fields
185+
-------------
186+
187+
A field with a ``Union`` of multiple non-``None`` types is treated as an
188+
ASN.1 ``CHOICE``. Each variant must have a distinct ASN.1 tag:
189+
190+
.. doctest::
191+
192+
>>> import typing
193+
>>> from cryptography.hazmat import asn1
194+
>>> @asn1.sequence
195+
... class Example:
196+
... value: typing.Union[int, bool, str]
197+
>>> asn1.decode_der(Example, asn1.encode_der(Example(value=42))).value
198+
42
199+
>>> asn1.decode_der(Example, asn1.encode_der(Example(value=True))).value
200+
True
201+
202+
When multiple alternatives share the same underlying type, a plain union
203+
can't distinguish them (``Union[int, int]`` is just ``int``). Wrap the types with
204+
:class:`~cryptography.hazmat.asn1.Variant` and add
205+
:class:`~cryptography.hazmat.asn1.Implicit` tags to differentiate
206+
between them:
207+
208+
.. doctest::
209+
210+
>>> import typing
211+
>>> from typing import Annotated
212+
>>> from cryptography.hazmat import asn1
213+
>>> @asn1.sequence
214+
... class Example:
215+
... field: typing.Union[
216+
... Annotated[asn1.Variant[int, typing.Literal["IntA"]], asn1.Implicit(0)],
217+
... Annotated[asn1.Variant[int, typing.Literal["IntB"]], asn1.Implicit(1)],
218+
... ]
219+
>>> obj = Example(field=asn1.Variant(9, "IntA"))
220+
>>> decoded = asn1.decode_der(Example, asn1.encode_der(obj))
221+
>>> decoded.field.value
222+
9
223+
>>> decoded.field.tag
224+
'IntA'
225+
226+
EXPLICIT and IMPLICIT tagging
227+
------------------------------
228+
229+
Use :class:`~cryptography.hazmat.asn1.Explicit` and :class:`~cryptography.hazmat.asn1.Implicit` annotations to apply ASN.1
230+
context-specific tags to fields:
231+
232+
.. doctest::
233+
234+
>>> from typing import Annotated
235+
>>> from cryptography.hazmat import asn1
236+
>>> @asn1.sequence
237+
... class Tagged:
238+
... explicit_field: Annotated[int, asn1.Explicit(0)]
239+
... implicit_field: Annotated[int, asn1.Implicit(1)]
240+
>>> encoded = asn1.encode_der(Tagged(explicit_field=5, implicit_field=10))
241+
>>> decoded = asn1.decode_der(Tagged, encoded)
242+
>>> decoded.explicit_field
243+
5
244+
>>> decoded.implicit_field
245+
10
246+
247+
Tagging is typically needed to disambiguate ``OPTIONAL`` fields that would
248+
otherwise share the same tag:
249+
250+
.. doctest::
251+
252+
>>> import typing
253+
>>> from typing import Annotated
254+
>>> from cryptography.hazmat import asn1
255+
>>> @asn1.sequence
256+
... class Example:
257+
... a: Annotated[typing.Union[int, None], asn1.Implicit(0)]
258+
... b: Annotated[typing.Union[int, None], asn1.Implicit(1)]
259+
>>> asn1.decode_der(Example, asn1.encode_der(Example(a=9, b=None))).a
260+
9
261+
>>> asn1.decode_der(Example, asn1.encode_der(Example(a=None, b=9))).b
262+
9
263+
264+
SEQUENCE OF and SET OF
265+
----------------------
266+
267+
Use :class:`list`\[T] for ``SEQUENCE OF`` and :class:`~cryptography.hazmat.asn1.SetOf` for
268+
``SET OF``:
269+
270+
.. doctest::
271+
272+
>>> import typing
273+
>>> from cryptography.hazmat import asn1
274+
>>> @asn1.sequence
275+
... class IntList:
276+
... values: typing.List[int]
277+
>>> decoded = asn1.decode_der(IntList, asn1.encode_der(IntList(values=[1, 2, 3])))
278+
>>> decoded.values
279+
[1, 2, 3]
280+
281+
``SET OF`` elements are sorted in DER encoding:
282+
283+
.. doctest::
284+
285+
>>> from cryptography.hazmat import asn1
286+
>>> @asn1.sequence
287+
... class IntSet:
288+
... values: asn1.SetOf[int]
289+
>>> decoded = asn1.decode_der(IntSet, asn1.encode_der(IntSet(values=asn1.SetOf([3, 1, 2]))))
290+
>>> decoded.values.as_list()
291+
[1, 2, 3]
292+
293+
Size constraints
294+
----------------
295+
296+
Use :class:`~cryptography.hazmat.asn1.Size` to restrict the length of collection and string fields:
297+
298+
.. doctest::
299+
300+
>>> import typing
301+
>>> from typing import Annotated
302+
>>> from cryptography.hazmat import asn1
303+
>>> @asn1.sequence
304+
... class BoundedList:
305+
... values: Annotated[typing.List[int], asn1.Size(min=1, max=4)]
306+
>>> asn1.encode_der(BoundedList(values=[1, 2]))
307+
b'0\x080\x06\x02\x01\x01\x02\x01\x02'
308+
309+
A real-world example
310+
--------------------
311+
312+
Here is a more complete example modeling the X.509 ``Validity`` structure:
313+
314+
.. code-block:: none
315+
316+
Validity ::= SEQUENCE {
317+
notBefore Time,
318+
notAfter Time }
319+
320+
Time ::= CHOICE {
321+
utcTime UTCTime,
322+
generalTime GeneralizedTime }
323+
324+
This translates to:
325+
326+
.. doctest::
327+
328+
>>> import typing
329+
>>> import datetime
330+
>>> from typing import Annotated
331+
>>> from cryptography.hazmat import asn1
332+
>>> @asn1.sequence
333+
... class Validity:
334+
... not_before: typing.Union[asn1.UTCTime, asn1.GeneralizedTime]
335+
... not_after: typing.Union[asn1.UTCTime, asn1.GeneralizedTime]
336+
>>> not_before = asn1.UTCTime(datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc))
337+
>>> not_after = asn1.UTCTime(datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc))
338+
>>> validity = Validity(not_before=not_before, not_after=not_after)
339+
>>> decoded = asn1.decode_der(Validity, asn1.encode_der(validity))
340+
>>> decoded.not_before.as_datetime().year
341+
2025
342+
>>> decoded.not_after.as_datetime().year
343+
2026

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ cryptographically
3636
de
3737
Debian
3838
deallocated
39+
declaratively
3940
decrypt
4041
decrypts
4142
Decrypts

0 commit comments

Comments
 (0)