Skip to content

Commit 07f2662

Browse files
authored
asn1: Add support for PrintableString (#13496)
* asn1: Add support for PrintableString Signed-off-by: Facundo Tuesca <[email protected]> * Add __repr__ to PrintableString Signed-off-by: Facundo Tuesca <[email protected]> * Remove PyBackedStr usage Signed-off-by: Facundo Tuesca <[email protected]> * Add test for PrintableString's __repr__ Signed-off-by: Facundo Tuesca <[email protected]> * Use `PyStringMethods::to_cow` instead of `to_str` Signed-off-by: Facundo Tuesca <[email protected]> * Remove unneeded `bind` call Signed-off-by: Facundo Tuesca <[email protected]> * Address review comments Signed-off-by: Facundo Tuesca <[email protected]> * Check PrintableString validity during __new__ Signed-off-by: Facundo Tuesca <[email protected]> --------- Signed-off-by: Facundo Tuesca <[email protected]>
1 parent adf4348 commit 07f2662

File tree

9 files changed

+112
-7
lines changed

9 files changed

+112
-7
lines changed

src/cryptography/hazmat/asn1/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
33
# for complete details.
44

5-
from cryptography.hazmat.asn1.asn1 import decode_der, encode_der, sequence
5+
from cryptography.hazmat.asn1.asn1 import (
6+
PrintableString,
7+
decode_der,
8+
encode_der,
9+
sequence,
10+
)
611

712
__all__ = [
13+
"PrintableString",
814
"decode_der",
915
"encode_der",
1016
"sequence",

src/cryptography/hazmat/asn1/asn1.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,6 @@ def sequence(cls: type[U]) -> type[U]:
115115
)(cls)
116116
_register_asn1_sequence(dataclass_cls)
117117
return dataclass_cls
118+
119+
120+
PrintableString = declarative_asn1.PrintableString

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ class AnnotatedTypeObject:
3434
def __new__(
3535
cls, annotated_type: AnnotatedType, value: typing.Any
3636
) -> AnnotatedTypeObject: ...
37+
def __repr__(self) -> str: ...
38+
def __eq__(self, other: object) -> bool: ...
39+
40+
class PrintableString:
41+
def __new__(cls, inner: str) -> PrintableString: ...
42+
def as_str(self) -> str: ...

src/rust/src/declarative_asn1/decode.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use asn1::Parser;
66
use pyo3::types::PyAnyMethods;
77

88
use crate::asn1::big_byte_slice_to_py_int;
9-
use crate::declarative_asn1::types::{AnnotatedType, Type};
9+
use crate::declarative_asn1::types::{AnnotatedType, PrintableString, Type};
1010
use crate::error::CryptographyError;
1111

1212
type ParseResult<T> = Result<T, CryptographyError>;
@@ -48,6 +48,15 @@ fn decode_pystr<'a>(
4848
Ok(pyo3::types::PyString::new(py, value.as_str()))
4949
}
5050

51+
fn decode_printable_string<'a>(
52+
py: pyo3::Python<'a>,
53+
parser: &mut Parser<'a>,
54+
) -> ParseResult<pyo3::Bound<'a, PrintableString>> {
55+
let value = parser.read_element::<asn1::PrintableString<'a>>()?.as_str();
56+
let inner = pyo3::types::PyString::new(py, value).unbind();
57+
Ok(pyo3::Bound::new(py, PrintableString { inner })?)
58+
}
59+
5160
pub(crate) fn decode_annotated_type<'a>(
5261
py: pyo3::Python<'a>,
5362
parser: &mut Parser<'a>,
@@ -78,5 +87,6 @@ pub(crate) fn decode_annotated_type<'a>(
7887
Type::PyInt() => Ok(decode_pyint(py, parser)?.into_any()),
7988
Type::PyBytes() => Ok(decode_pybytes(py, parser)?.into_any()),
8089
Type::PyStr() => Ok(decode_pystr(py, parser)?.into_any()),
90+
Type::PrintableString() => Ok(decode_printable_string(py, parser)?.into_any()),
8191
}
8292
}

src/rust/src/declarative_asn1/encode.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use asn1::{SimpleAsn1Writable, Writer};
66
use pyo3::types::PyAnyMethods;
77

8-
use crate::declarative_asn1::types::{AnnotatedType, AnnotatedTypeObject, Type};
8+
use crate::declarative_asn1::types::{AnnotatedType, AnnotatedTypeObject, PrintableString, Type};
99

1010
fn write_value<T: SimpleAsn1Writable>(
1111
writer: &mut Writer<'_>,
@@ -73,6 +73,20 @@ impl asn1::Asn1Writable for AnnotatedTypeObject<'_> {
7373
let asn1_string: asn1::Utf8String<'_> = asn1::Utf8String::new(&val);
7474
write_value(writer, &asn1_string)
7575
}
76+
Type::PrintableString() => {
77+
let val: &pyo3::Bound<'_, PrintableString> = value
78+
.downcast()
79+
.map_err(|_| asn1::WriteError::AllocationError)?;
80+
let inner_str = val
81+
.get()
82+
.inner
83+
.to_cow(py)
84+
.map_err(|_| asn1::WriteError::AllocationError)?;
85+
let printable_string: asn1::PrintableString<'_> =
86+
asn1::PrintableString::new(&inner_str)
87+
.ok_or(asn1::WriteError::AllocationError)?;
88+
write_value(writer, &printable_string)
89+
}
7690
}
7791
}
7892
}

src/rust/src/declarative_asn1/types.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
33
// for complete details.
44

5+
use asn1::PrintableString as Asn1PrintableString;
56
use pyo3::types::PyAnyMethods;
67
use pyo3::{IntoPyObject, PyTypeInfo};
78

@@ -32,6 +33,9 @@ pub enum Type {
3233
/// `str` -> `UTF8String`
3334
#[pyo3(constructor = ())]
3435
PyStr(),
36+
/// PrintableString (`str`)
37+
#[pyo3(constructor = ())]
38+
PrintableString(),
3539
}
3640

3741
/// A type that we know how to encode/decode, along with any
@@ -70,6 +74,40 @@ impl Annotation {
7074
}
7175
}
7276

77+
#[derive(pyo3::FromPyObject)]
78+
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.asn1")]
79+
pub struct PrintableString {
80+
pub(crate) inner: pyo3::Py<pyo3::types::PyString>,
81+
}
82+
83+
#[pyo3::pymethods]
84+
impl PrintableString {
85+
#[new]
86+
#[pyo3(signature = (inner,))]
87+
fn new(py: pyo3::Python<'_>, inner: pyo3::Py<pyo3::types::PyString>) -> pyo3::PyResult<Self> {
88+
if Asn1PrintableString::new(&inner.to_cow(py)?).is_none() {
89+
return Err(pyo3::exceptions::PyValueError::new_err(format!(
90+
"invalid PrintableString: {inner}"
91+
)));
92+
}
93+
94+
Ok(PrintableString { inner })
95+
}
96+
97+
#[pyo3(signature = ())]
98+
pub fn as_str(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyString>> {
99+
Ok(self.inner.clone_ref(py))
100+
}
101+
102+
fn __eq__(&self, py: pyo3::Python<'_>, other: pyo3::PyRef<'_, Self>) -> pyo3::PyResult<bool> {
103+
(**self.inner.bind(py)).eq(other.inner.bind(py))
104+
}
105+
106+
pub fn __repr__(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<String> {
107+
Ok(format!("PrintableString({})", self.inner.bind(py).repr()?))
108+
}
109+
}
110+
73111
/// Utility function for converting builtin Python types
74112
/// to their Rust `Type` equivalent.
75113
#[pyo3::pyfunction]
@@ -85,6 +123,8 @@ pub fn non_root_python_to_rust<'p>(
85123
Type::PyStr().into_pyobject(py)
86124
} else if class.is(pyo3::types::PyBytes::type_object(py)) {
87125
Type::PyBytes().into_pyobject(py)
126+
} else if class.is(PrintableString::type_object(py)) {
127+
Type::PrintableString().into_pyobject(py)
88128
} else {
89129
Err(pyo3::exceptions::PyTypeError::new_err(format!(
90130
"cannot handle type: {class:?}"
@@ -131,5 +171,5 @@ pub(crate) fn python_class_to_annotated<'p>(
131171
#[pyo3::pymodule(gil_used = false)]
132172
pub(crate) mod types {
133173
#[pymodule_export]
134-
use super::{AnnotatedType, Annotation, Type};
174+
use super::{AnnotatedType, Annotation, PrintableString, Type};
135175
}

src/rust/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ mod _rust {
124124

125125
#[pymodule_export]
126126
use crate::declarative_asn1::types::{
127-
non_root_python_to_rust, AnnotatedType, Annotation, Type,
127+
non_root_python_to_rust, AnnotatedType, Annotation, PrintableString, Type,
128128
};
129129
}
130130

tests/hazmat/asn1/test_api.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,20 @@
99
import cryptography.hazmat.asn1 as asn1
1010

1111

12-
class TestClassAPI:
12+
class TestTypesAPI:
13+
def test_repr_printable_string(self) -> None:
14+
my_string = "MyString"
15+
assert (
16+
repr(asn1.PrintableString(my_string))
17+
== f"PrintableString({my_string!r})"
18+
)
19+
20+
def test_invalid_printable_string(self) -> None:
21+
with pytest.raises(ValueError, match="invalid PrintableString: café"):
22+
asn1.PrintableString("café")
23+
24+
25+
class TestSequenceAPI:
1326
def test_fail_unsupported_field(self) -> None:
1427
# Not a sequence
1528
class Unsupported:

tests/hazmat/asn1/test_serialization.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ def _comparable_dataclass(cls: typing.Type[U]) -> typing.Type[U]:
3434
)(cls)
3535

3636

37+
# Checks that the encoding-decoding roundtrip results
38+
# in the expected values and is consistent.
3739
def assert_roundtrips(
38-
test_cases: typing.List[typing.Tuple[typing.Any, bytes]],
40+
test_cases: typing.List[typing.Tuple[U, bytes]],
3941
) -> None:
4042
for obj, obj_bytes in test_cases:
4143
encoded = asn1.encode_der(obj)
@@ -105,6 +107,17 @@ def test_string(self) -> None:
105107
)
106108

107109

110+
class TestPrintableString:
111+
def test_ok_printable_string(self) -> None:
112+
assert_roundtrips(
113+
[
114+
(asn1.PrintableString(""), b"\x13\x00"),
115+
(asn1.PrintableString("hello"), b"\x13\x05hello"),
116+
(asn1.PrintableString("Test User 1"), b"\x13\x0bTest User 1"),
117+
]
118+
)
119+
120+
108121
class TestSequence:
109122
def test_ok_sequence_single_field(self) -> None:
110123
@asn1.sequence

0 commit comments

Comments
 (0)