|
| 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 |
0 commit comments