diff --git a/src/cryptography/hazmat/asn1/asn1.py b/src/cryptography/hazmat/asn1/asn1.py index 50b542fb3d46..acea4fa433df 100644 --- a/src/cryptography/hazmat/asn1/asn1.py +++ b/src/cryptography/hazmat/asn1/asn1.py @@ -4,6 +4,7 @@ from __future__ import annotations +import builtins import dataclasses import sys import types @@ -127,6 +128,11 @@ def _normalize_field_type( raise TypeError( "union types other than `X | None` are currently not supported" ) + elif get_type_origin(field_type) is builtins.list: + inner_type = _normalize_field_type( + get_type_args(field_type)[0], field_name + ) + rust_field_type = declarative_asn1.Type.SequenceOf(inner_type) else: rust_field_type = declarative_asn1.non_root_python_to_rust(field_type) diff --git a/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi b/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi index 4bddb043294d..736be4cbd8a6 100644 --- a/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi +++ b/src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi @@ -13,6 +13,7 @@ def non_root_python_to_rust(cls: type) -> Type: ... # annotations like this: class Type: Sequence: typing.ClassVar[type] + SequenceOf: typing.ClassVar[type] Option: typing.ClassVar[type] PyBool: typing.ClassVar[type] PyInt: typing.ClassVar[type] diff --git a/src/rust/src/declarative_asn1/decode.rs b/src/rust/src/declarative_asn1/decode.rs index 7b61e033b7fe..73ea1d2fefd2 100644 --- a/src/rust/src/declarative_asn1/decode.rs +++ b/src/rust/src/declarative_asn1/decode.rs @@ -4,6 +4,7 @@ use asn1::Parser; use pyo3::types::PyAnyMethods; +use pyo3::types::PyListMethods; use crate::asn1::big_byte_slice_to_py_int; use crate::declarative_asn1::types::{ @@ -151,6 +152,19 @@ pub(crate) fn decode_annotated_type<'a>( Ok(val) })? } + Type::SequenceOf(cls) => { + let seqof_parse_result = read_value::>(parser, encoding)?; + + seqof_parse_result.parse(|d| -> ParseResult> { + let inner_ann_type = cls.get(); + let list = pyo3::types::PyList::empty(py); + while !d.is_empty() { + let val = decode_annotated_type(py, d, inner_ann_type)?; + list.append(val)?; + } + Ok(list.into_any()) + })? + } Type::Option(cls) => { let inner_tag = type_to_tag(cls.get().inner.get(), encoding); match parser.peek_tag() { diff --git a/src/rust/src/declarative_asn1/encode.rs b/src/rust/src/declarative_asn1/encode.rs index fee30f18df18..dcdc4661868d 100644 --- a/src/rust/src/declarative_asn1/encode.rs +++ b/src/rust/src/declarative_asn1/encode.rs @@ -4,6 +4,7 @@ use asn1::{SimpleAsn1Writable, Writer}; use pyo3::types::PyAnyMethods; +use pyo3::types::PyListMethods; use crate::declarative_asn1::types::{ AnnotatedType, AnnotatedTypeObject, Encoding, GeneralizedTime, PrintableString, Type, UtcTime, @@ -70,6 +71,18 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> { }), encoding, ), + Type::SequenceOf(cls) => { + let values: Vec> = value + .cast::() + .map_err(|_| asn1::WriteError::AllocationError)? + .iter() + .map(|e| AnnotatedTypeObject { + annotated_type: cls.get(), + value: e, + }) + .collect(); + write_value(writer, &asn1::SequenceOfWriter::new(values), encoding) + } Type::Option(cls) => { if !value.is_none() { let inner_object = AnnotatedTypeObject { diff --git a/src/rust/src/declarative_asn1/types.rs b/src/rust/src/declarative_asn1/types.rs index b50a354c7d40..ef886c441e7d 100644 --- a/src/rust/src/declarative_asn1/types.rs +++ b/src/rust/src/declarative_asn1/types.rs @@ -19,6 +19,8 @@ pub enum Type { /// The first element is the Python class that represents the sequence, /// the second element is a dict of the (already converted) fields of the class. Sequence(pyo3::Py, pyo3::Py), + /// SEQUENCEOF (`list[`T`]`) + SequenceOf(pyo3::Py), /// OPTIONAL (`T | None`) Option(pyo3::Py), @@ -285,6 +287,7 @@ pub(crate) fn python_class_to_annotated<'p>( pub(crate) fn type_to_tag(t: &Type, encoding: &Option>) -> asn1::Tag { let inner_tag = match t { Type::Sequence(_, _) => asn1::Sequence::TAG, + Type::SequenceOf(_) => asn1::Sequence::TAG, Type::Option(t) => type_to_tag(t.get().inner.get(), encoding), Type::PyBool() => bool::TAG, Type::PyInt() => asn1::BigInt::TAG, diff --git a/tests/hazmat/asn1/test_api.py b/tests/hazmat/asn1/test_api.py index 9b3869a46b38..88ec59f9276b 100644 --- a/tests/hazmat/asn1/test_api.py +++ b/tests/hazmat/asn1/test_api.py @@ -274,6 +274,9 @@ def test_fields_of_variant_type(self) -> None: opt = declarative_asn1.Type.Option(ann_type) assert opt._0 == ann_type + seq_of = declarative_asn1.Type.SequenceOf(ann_type) + assert seq_of._0 is ann_type + def test_fields_of_variant_encoding(self) -> None: from cryptography.hazmat.bindings._rust import declarative_asn1 diff --git a/tests/hazmat/asn1/test_serialization.py b/tests/hazmat/asn1/test_serialization.py index 73d501773ead..da8a4d7bc41c 100644 --- a/tests/hazmat/asn1/test_serialization.py +++ b/tests/hazmat/asn1/test_serialization.py @@ -277,6 +277,42 @@ class Example: ] ) + def test_ok_sequenceof_simple_type(self) -> None: + @asn1.sequence + @_comparable_dataclass + class Example: + a: typing.List[int] + + assert_roundtrips( + [ + ( + Example(a=[1, 2, 3, 4]), + b"\x30\x0e\x30\x0c\x02\x01\x01\x02\x01\x02\x02\x01\x03\x02\x01\x04", + ) + ] + ) + + def test_ok_sequenceof_user_defined_type(self) -> None: + @asn1.sequence + @_comparable_dataclass + class MyType: + a: int + b: bool + + @asn1.sequence + @_comparable_dataclass + class Example: + a: typing.List[MyType] + + assert_roundtrips( + [ + ( + Example(a=[MyType(a=1, b=True), MyType(a=2, b=False)]), + b"\x30\x12\x30\x10\x30\x06\x02\x01\x01\x01\x01\xff\x30\x06\x02\x01\x02\x01\x01\x00", + ) + ] + ) + def test_ok_sequence_with_optionals(self) -> None: @asn1.sequence @_comparable_dataclass @@ -350,11 +386,14 @@ class Example: d: typing.Union[asn1.PrintableString, None] e: typing.Union[asn1.UtcTime, None] f: typing.Union[asn1.GeneralizedTime, None] + g: typing.Union[typing.List[int], None] assert_roundtrips( [ ( - Example(a=None, b=None, c=None, d=None, e=None, f=None), + Example( + a=None, b=None, c=None, d=None, e=None, f=None, g=None + ), b"\x30\x00", ) ]