Skip to content

Commit d8f766e

Browse files
committed
asn1: Add support for SIZE to SEQUENCE OF
Signed-off-by: Facundo Tuesca <[email protected]>
1 parent 7ee25b1 commit d8f766e

File tree

9 files changed

+197
-5
lines changed

9 files changed

+197
-5
lines changed

src/cryptography/hazmat/asn1/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
GeneralizedTime,
99
Implicit,
1010
PrintableString,
11+
Size,
1112
UtcTime,
1213
decode_der,
1314
encode_der,
@@ -20,6 +21,7 @@
2021
"GeneralizedTime",
2122
"Implicit",
2223
"PrintableString",
24+
"Size",
2325
"UtcTime",
2426
"decode_der",
2527
"encode_der",

src/cryptography/hazmat/asn1/asn1.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def _extract_annotation(
6464
) -> declarative_asn1.Annotation:
6565
default = None
6666
encoding = None
67+
size = None
6768
for raw_annotation in metadata:
6869
if isinstance(raw_annotation, Default):
6970
if default is not None:
@@ -79,10 +80,18 @@ def _extract_annotation(
7980
f"'{field_name}'"
8081
)
8182
encoding = raw_annotation
83+
elif isinstance(raw_annotation, declarative_asn1.Size):
84+
if size is not None:
85+
raise TypeError(
86+
f"multiple SIZE annotations found in field '{field_name}'"
87+
)
88+
size = raw_annotation
8289
else:
8390
raise TypeError(f"unsupported annotation: {raw_annotation}")
8491

85-
return declarative_asn1.Annotation(default=default, encoding=encoding)
92+
return declarative_asn1.Annotation(
93+
default=default, encoding=encoding, size=size
94+
)
8695

8796

8897
def _normalize_field_type(
@@ -217,6 +226,7 @@ class Default(typing.Generic[U]):
217226

218227
Explicit = declarative_asn1.Encoding.Explicit
219228
Implicit = declarative_asn1.Encoding.Implicit
229+
Size = declarative_asn1.Size
220230

221231
PrintableString = declarative_asn1.PrintableString
222232
UtcTime = declarative_asn1.UtcTime

src/cryptography/hazmat/bindings/_rust/declarative_asn1.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ class Type:
2323
class Annotation:
2424
default: typing.Any | None
2525
encoding: Encoding | None
26+
size: Size | None
2627
def __new__(
2728
cls,
2829
default: typing.Any | None = None,
2930
encoding: Encoding | None = None,
31+
size: Size | None = None,
3032
) -> Annotation: ...
3133
def is_empty(self) -> bool: ...
3234

@@ -36,6 +38,14 @@ class Encoding:
3638
Implicit: typing.ClassVar[type]
3739
Explicit: typing.ClassVar[type]
3840

41+
class Size:
42+
min: int
43+
max: int | None
44+
45+
def __new__(cls, min: int, max: int | None) -> Size: ...
46+
@staticmethod
47+
def exact(n: int) -> Size: ...
48+
3949
class AnnotatedType:
4050
inner: Type
4151
annotation: Annotation

src/rust/src/declarative_asn1/decode.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,19 @@ pub(crate) fn decode_annotated_type<'a>(
162162
let val = decode_annotated_type(py, d, inner_ann_type)?;
163163
list.append(val)?;
164164
}
165+
if let Some(size) = &ann_type.annotation.get().size {
166+
let list_len = list.len();
167+
let min = size.get().min;
168+
let max = size.get().max.unwrap_or(usize::MAX);
169+
if list_len < min || list_len > max {
170+
return Err(CryptographyError::Py(
171+
pyo3::exceptions::PyValueError::new_err(format!(
172+
"SEQUENCE OF has size {0}, expected size in [{1}, {2}]",
173+
list_len, min, max
174+
)),
175+
));
176+
}
177+
}
165178
Ok(list.into_any())
166179
})?
167180
}

src/rust/src/declarative_asn1/encode.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> {
8181
value: e,
8282
})
8383
.collect();
84+
85+
if let Some(size) = &annotated_type.annotation.get().size {
86+
let min = size.get().min;
87+
let max = size.get().max.unwrap_or(usize::MAX);
88+
if values.len() < min || values.len() > max {
89+
return Err(asn1::WriteError::AllocationError);
90+
}
91+
}
8492
write_value(writer, &asn1::SequenceOfWriter::new(values), encoding)
8593
}
8694
Type::Option(cls) => {

src/rust/src/declarative_asn1/types.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,29 @@ pub struct Annotation {
7474
pub(crate) default: Option<pyo3::Py<pyo3::types::PyAny>>,
7575
#[pyo3(get)]
7676
pub(crate) encoding: Option<pyo3::Py<Encoding>>,
77+
#[pyo3(get)]
78+
pub(crate) size: Option<pyo3::Py<Size>>,
7779
}
7880

7981
#[pyo3::pymethods]
8082
impl Annotation {
8183
#[new]
82-
#[pyo3(signature = (default = None, encoding = None))]
84+
#[pyo3(signature = (default = None, encoding = None, size = None))]
8385
fn new(
8486
default: Option<pyo3::Py<pyo3::types::PyAny>>,
8587
encoding: Option<pyo3::Py<Encoding>>,
88+
size: Option<pyo3::Py<Size>>,
8689
) -> Self {
87-
Self { default, encoding }
90+
Self {
91+
default,
92+
encoding,
93+
size,
94+
}
8895
}
8996

9097
#[pyo3(signature = ())]
9198
fn is_empty(&self) -> bool {
92-
self.default.is_none() && self.encoding.is_none()
99+
self.default.is_none() && self.encoding.is_none() && self.size.is_none()
93100
}
94101
}
95102

@@ -99,6 +106,28 @@ pub enum Encoding {
99106
Explicit(u32),
100107
}
101108

109+
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.asn1")]
110+
pub struct Size {
111+
pub min: usize,
112+
pub max: Option<usize>,
113+
}
114+
115+
#[pyo3::pymethods]
116+
impl Size {
117+
#[new]
118+
fn new(min: usize, max: Option<usize>) -> Self {
119+
Size { min, max }
120+
}
121+
122+
#[staticmethod]
123+
fn exact(n: usize) -> Self {
124+
Size {
125+
min: n,
126+
max: Some(n),
127+
}
128+
}
129+
}
130+
102131
#[derive(pyo3::FromPyObject)]
103132
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.asn1")]
104133
pub struct PrintableString {
@@ -263,6 +292,7 @@ fn non_root_type_to_annotated<'p>(
263292
annotation: Annotation {
264293
default: None,
265294
encoding: None,
295+
size: None,
266296
}
267297
.into_pyobject(py)?
268298
.unbind(),
@@ -328,6 +358,7 @@ mod tests {
328358
annotation: Annotation {
329359
default: None,
330360
encoding: None,
361+
size: None,
331362
}
332363
.into_pyobject(py)
333364
.unwrap()
@@ -342,6 +373,7 @@ mod tests {
342373
annotation: Annotation {
343374
default: None,
344375
encoding: None,
376+
size: None,
345377
}
346378
.into_pyobject(py)
347379
.unwrap()

src/rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ mod _rust {
152152
#[pymodule_export]
153153
use crate::declarative_asn1::types::{
154154
non_root_python_to_rust, AnnotatedType, Annotation, Encoding, GeneralizedTime,
155-
PrintableString, Type, UtcTime,
155+
PrintableString, Size, Type, UtcTime,
156156
};
157157
}
158158

tests/hazmat/asn1/test_api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,18 @@ def test_fail_multiple_explicit_annotations(self) -> None:
228228
class Example:
229229
invalid: Annotated[int, asn1.Explicit(0), asn1.Explicit(1)]
230230

231+
def test_fail_multiple_size_annotations(self) -> None:
232+
with pytest.raises(
233+
TypeError,
234+
match="multiple SIZE annotations found in field 'invalid'",
235+
):
236+
237+
@asn1.sequence
238+
class Example:
239+
invalid: Annotated[
240+
int, asn1.Size(min=1, max=2), asn1.Size(min=1, max=2)
241+
]
242+
231243
def test_fail_optional_with_default_field(self) -> None:
232244
with pytest.raises(
233245
TypeError,

tests/hazmat/asn1/test_serialization.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,111 @@ class Example:
313313
]
314314
)
315315

316+
def test_ok_sequenceof_size_restriction(self) -> None:
317+
@asn1.sequence
318+
@_comparable_dataclass
319+
class Example:
320+
a: Annotated[typing.List[int], asn1.Size(min=1, max=4)]
321+
322+
assert_roundtrips(
323+
[
324+
(
325+
Example(a=[1, 2, 3, 4]),
326+
b"\x30\x0e\x30\x0c\x02\x01\x01\x02\x01\x02\x02\x01\x03\x02\x01\x04",
327+
)
328+
]
329+
)
330+
331+
def test_ok_sequenceof_size_restriction_no_max(self) -> None:
332+
@asn1.sequence
333+
@_comparable_dataclass
334+
class Example:
335+
a: Annotated[typing.List[int], asn1.Size(min=1, max=None)]
336+
337+
assert_roundtrips(
338+
[
339+
(
340+
Example(a=[1, 2, 3, 4]),
341+
b"\x30\x0e\x30\x0c\x02\x01\x01\x02\x01\x02\x02\x01\x03\x02\x01\x04",
342+
)
343+
]
344+
)
345+
346+
def test_ok_sequenceof_size_restriction_exact(self) -> None:
347+
@asn1.sequence
348+
@_comparable_dataclass
349+
class Example:
350+
a: Annotated[typing.List[int], asn1.Size.exact(4)]
351+
352+
assert_roundtrips(
353+
[
354+
(
355+
Example(a=[1, 2, 3, 4]),
356+
b"\x30\x0e\x30\x0c\x02\x01\x01\x02\x01\x02\x02\x01\x03\x02\x01\x04",
357+
)
358+
]
359+
)
360+
361+
def test_fail_sequenceof_size_too_big(self) -> None:
362+
@asn1.sequence
363+
@_comparable_dataclass
364+
class Example:
365+
a: Annotated[typing.List[int], asn1.Size(min=1, max=2)]
366+
367+
with pytest.raises(
368+
ValueError,
369+
match=re.escape("SEQUENCE OF has size 4, expected size in [1, 2]"),
370+
):
371+
asn1.decode_der(
372+
Example,
373+
b"\x30\x0e\x30\x0c\x02\x01\x01\x02\x01\x02\x02\x01\x03\x02\x01\x04",
374+
)
375+
376+
with pytest.raises(
377+
ValueError,
378+
):
379+
asn1.encode_der(Example(a=[1, 2, 3, 4]))
380+
381+
def test_fail_sequenceof_size_too_small(self) -> None:
382+
@asn1.sequence
383+
@_comparable_dataclass
384+
class Example:
385+
a: Annotated[typing.List[int], asn1.Size(min=5, max=6)]
386+
387+
with pytest.raises(
388+
ValueError,
389+
match=re.escape("SEQUENCE OF has size 4, expected size in [5, 6]"),
390+
):
391+
asn1.decode_der(
392+
Example,
393+
b"\x30\x0e\x30\x0c\x02\x01\x01\x02\x01\x02\x02\x01\x03\x02\x01\x04",
394+
)
395+
396+
with pytest.raises(
397+
ValueError,
398+
):
399+
asn1.encode_der(Example(a=[1, 2, 3, 4]))
400+
401+
def test_fail_sequenceof_size_not_exact(self) -> None:
402+
@asn1.sequence
403+
@_comparable_dataclass
404+
class Example:
405+
a: Annotated[typing.List[int], asn1.Size.exact(5)]
406+
407+
with pytest.raises(
408+
ValueError,
409+
match=re.escape("SEQUENCE OF has size 4, expected size in [5, 5]"),
410+
):
411+
asn1.decode_der(
412+
Example,
413+
b"\x30\x0e\x30\x0c\x02\x01\x01\x02\x01\x02\x02\x01\x03\x02\x01\x04",
414+
)
415+
416+
with pytest.raises(
417+
ValueError,
418+
):
419+
asn1.encode_der(Example(a=[1, 2, 3, 4]))
420+
316421
def test_ok_sequence_with_optionals(self) -> None:
317422
@asn1.sequence
318423
@_comparable_dataclass

0 commit comments

Comments
 (0)